diff --git a/README.md b/README.md
index 3345710..b4735f4 100644
--- a/README.md
+++ b/README.md
@@ -1,244 +1,96 @@
-# External Phone Provider: Azure Function Sample
+# External Phone Provider Azure Function Sample
-Connect Microsoft Entra ID to your phone provider to send one-time passwords by SMS or voice.
-This External Phone Provider (EPP) sample sets up **one endpoint in one Azure region**.
+Connect Microsoft Entra ID to a phone provider for SMS and voice one-time passwords.
+Guided setup deploys **one regional endpoint** using JavaScript, C#, or Python.
-You register an application, run a guided setup script, then connect and test your provider.
-An administrator activates the endpoint only after those checks pass. You do not need to clone
-this repository, build a package yourself, or follow all three language guides.
+Start in a separate nonproduction environment and review [requirements](#requirements).
-The same Function endpoint supports both SMS and voice. Each request's `channel` selects its
-configured provider URL, and the SMS and voice URLs can differ. Validate every channel you intend
-to activate.
+## Table of Contents
-The screenshots show the Azure portal. You can also register the application
-in the Microsoft Entra admin center. Identifying details are hidden; your portal layout may differ.
+- [Features](#features)
+- [Requirements](#requirements)
+- [Architecture](#architecture)
+- [Get started](#get-started)
+- [Guidance](#guidance)
+ - [Deploy and configure](#deploy-and-configure)
+ - [Operate](#operate)
+ - [Develop](#develop)
+ - [Advanced topics](#advanced-topics)
+- [Getting help](#getting-help)
-## 1. Get ready
+## Features
-Start in a **dedicated nonproduction tenant and subscription**.
+- JavaScript, C#, and Python implementations of one [HTTP and trust contract](docs/CONTRACT.md).
+- Guided Azure deployment with Easy Auth, Key Vault, and monitoring.
+- No-send evaluation for authentication and decryption checks.
+- Provider-neutral [phone-service integration](docs/BRING-YOUR-OWN-PROVIDER.md).
-Before purchasing or deploying, confirm access to a supported provider's EPP integration through
-[Microsoft Security Store](https://securitystore.microsoft.com/private-solutions). Arrange
-Microsoft's approved testing and activation procedure with your tenant administrator.
-Buying an offer does not enable the tenant feature or deploy this sample. If access or the
-approved procedure is unavailable, **stop here** and follow the
-[access and support guidance](docs/ONBOARDING.md#before-purchasing-or-deploying).
+## Requirements
-In Security Store, open **Private solutions** and select your approved subscription to review
-its available offers. See [sample offers](docs/ONBOARDING.md#before-purchasing-or-deploying) in the
-detailed guide; a listing does not guarantee support in this setup script.
+- Confirm [provider access and Microsoft's approved test and activation process](docs/GETTING-STARTED.md#1-get-ready).
+- Prepare a nonproduction tenant, subscription, app registration, and unique resource prefix.
+- Check [roles, permissions, tools, regional availability, and quota](setup/docs/README.md#prerequisites-for-step-2).
+- Plan for Azure, telemetry, and provider charges; the [cost estimate](docs/COST-CALCULATOR.md) is not a bill.
+- Keep Easy Auth enabled for the approved Microsoft caller.
-Use the [SMS cost estimation guide](docs/COST-CALCULATOR.md) to create a planning estimate,
-then confirm destination rates, fees, and final costs with your provider.
+## Architecture
-Have these ready:
+
-- **Provider account:** complete the provider's account and sender registration for your chosen
- SMS or voice service. Check [guided provider support](docs/ONBOARDING.md#provider-credential-names).
-- **Shell:** Azure Cloud Shell in PowerShell mode, or PowerShell 7+ with an Azure CLI user login.
- Azure CLI 2.60.0+ is required for Flex Consumption (FC1), or 2.48.1+ for Premium (EP1).
- Allow access to GitHub, Azure, Microsoft Graph, Key Vault, and the deployment endpoints.
- C# also needs the .NET 8 SDK and NuGet access.
-- **Administrators:** an Azure operator with subscription deployment, resource-provider registration,
- and role-assignment permissions; an Entra Privileged Role Administrator for setup; and an
- Authentication Policy Administrator for later activation. These are separate permissions.
-- **Azure region and budget:** check availability and subscription quota for your chosen Linux
- plan. Azure resources and provider services can incur charges.
+*Easy Auth checks the Microsoft caller before the Function decrypts the request.
+Live success confirms provider acceptance; confirm delivery separately. Evaluation sends nothing.
+See [architecture details](TECHNICAL.md#single-region-architecture).
+[Open the full-size diagram](docs/images/single-region-architecture.png).*
-Review the [full prerequisites](setup/docs/README.md#prerequisites-for-step-2), including the
-required Entra allowed-tenants preview, before running setup. Microsoft Graph operations reuse
-the Azure CLI login; no Graph PowerShell SDK or separate Graph sign-in is required. The script
-can install Bicep after confirmation; Azure CLI must already be installed.
+## Get started
-Choose an **onboarding owner** in your organization to coordinate the administrators, provider,
-and Microsoft support. This is a sample, not production certification: it does not provide
-automatic send retries, duplicate-send protection, or automatic certificate renewal.
-Review the [production limitations](docs/CONTRACT.md#production-limitations) with that owner.
+Follow the **[five-step setup guide](docs/GETTING-STARTED.md)** in order:
-## 2. Register your application
+1. [Get ready](docs/GETTING-STARTED.md#1-get-ready): confirm access, approvals, permissions, and budget.
+2. [Register the app](docs/GETTING-STARTED.md#2-register-your-application): save the tenant and client IDs.
+3. [Deploy](docs/GETTING-STARTED.md#3-run-guided-setup): choose the provider, runtime, and plan.
+4. [Connect and test](docs/GETTING-STARTED.md#4-connect-your-provider-and-test): configure provider access and run approved checks.
+5. [Activate and hand over](docs/GETTING-STARTED.md#5-activate-and-hand-over): activate after validation and assign owners.
-In the customer tenant's **Microsoft Entra admin center > App registrations > New registration**:
+Before live use, review [production readiness](docs/CONTRACT.md#production-limitations).
-1. Give the application a recognizable name.
-2. Select **Accounts in this organizational directory only**. Leave the redirect URI blank,
- then select **Register**. Setup will make the app organizational multitenant and restrict
- its allowed tenants after you approve the deployment plan.
-3. On **Overview**, save the **Directory (tenant) ID** and **Application (client) ID** privately.
- Use the client ID, not the Object ID.
+
+## Guidance
-
+### Deploy and configure
-*Some portal versions label the account type **Single tenant only**. Choose your own directory.
-The example above is an unsaved form, not an application you can reuse.*
+- [Five-step setup](docs/GETTING-STARTED.md)
+- [Configuration and validation](docs/ONBOARDING.md)
+- [Setup reference](setup/docs/README.md)
+- [Troubleshooting](setup/docs/Troubleshooting.md)
-Use these field labels to find the right IDs:
+### Operate
-
+- [Function monitoring and alerts](docs/MONITORING.md)
+- [Application Insights collection](docs/APPLICATION-INSIGHTS.md)
+- [Microsoft Entra ID monitoring](docs/SIGN-IN-MONITORING.md)
+- [Certificate renewal and rollback](setup/docs/README.md#encryption-certificate-lifecycle)
+- [Provider cost estimate](docs/COST-CALCULATOR.md)
-*Use **Application (client) ID** for `-ApplicationId` and **Directory (tenant) ID** for `-TenantId`.
-The **Object ID** is not either of these setup inputs. All ID values are hidden in this example.*
+### Develop
-Do not add a client secret, API permission, or app role yourself; setup handles the remaining
-configuration. See [application registration details](setup/docs/README.md#step-1---manually-create-the-application).
+- Runtime guides: [JavaScript](javascript/README.md), [C#](dotnet/README.md), [Python](python/README.md)
+- [Architecture and settings](TECHNICAL.md)
+- [HTTP contract](docs/CONTRACT.md)
+- [Log schema](docs/LOGGING.md)
+- [Phone-service integration](docs/BRING-YOUR-OWN-PROVIDER.md)
-Also keep your Azure subscription ID handy. The
-[setup values worksheet](docs/ONBOARDING.md#inputs-you-supply-to-setup) explains where to find
-each value. Use a new dedicated application and resource prefix for another independent endpoint;
-changing the provider or region on a rerun does not create a separate deployment.
+
+### Advanced topics
-## 3. Run guided setup
+- [Multi-region routing](docs/FRONTDOOR.md)
+- [Provider fallback](docs/MULTI-PROVIDER-IMPLEMENTATION.md)
-Open **PowerShell 7** in a folder where you want to keep the script and deployment summary.
-Download the script:
+## Getting help
-```powershell
-Invoke-WebRequest `
- -Uri 'https://raw.githubusercontent.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/main/setup/Setup-Epp.ps1' `
- -OutFile .\Setup-Epp.ps1
-```
+- [Provider access, testing, and activation](docs/ONBOARDING.md#before-purchasing-or-deploying)
+- [Setup and endpoint troubleshooting](setup/docs/Troubleshooting.md)
+- [Repository issues](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/issues)
-Review the downloaded script, then run it:
-
-```powershell
-.\Setup-Epp.ps1
-```
-
-On a shared or multi-account workstation, use `.\Setup-Epp.ps1 -ForceAuthentication` to request
-an explicit Azure CLI device-code sign-in. Microsoft Graph reuses that CLI session.
-
-Setup first lists cached Azure CLI user subscriptions. Select the customer tenant and subscription,
-then select or enter the dedicated application client ID and choose an Azure region. Refresh or
-sign in when the expected account is missing.
-
-Choose the provider route and enter or confirm the provider **by name**, then choose **one**
-platform: Node.js (JavaScript), .NET (C#), or Python. Setup configures both SMS and voice URLs;
-there is no channel prompt. It downloads and verifies the package, builds it if needed, and
-deploys it for you. You do not need a separate ZIP upload or local settings file.
-
-Choose **FC1** for on-demand hosting that can scale to zero, or **EP1** for paid warm capacity.
-FC1 can cold-start, and its free usage grant does not make the whole deployment free.
-See [hosting plan details](setup/docs/README.md#service-plan-selection).
-In **Tenant scope**, choose Global or EU for your provider route. This does not choose your
-Azure region or guarantee data residency.
-For the resource prefix, use 2-8 lowercase letters or digits, starting with a letter.
-
-Complete any required CLI sign-in, MFA, or administrator consent yourself. Setup checks
-prerequisites and prepares the deployment plan; resolve any reported failure before continuing.
-
-For a new application, setup creates its encryption certificate. If encryption credentials
-already exist, follow [certificate selection](setup/docs/README.md#encryption-certificate-selection)
-to reuse one exact Key Vault certificate version or approve replacement. Coordinate maintenance
-first: changing these credentials affects every endpoint sharing the application.
-
-**Review the complete deployment plan before typing `Yes`.** Check the accounts, region,
-SMS and voice provider URLs, platform, plan, cache settings, resource names, permissions, and
-the exact certificate key IDs to keep or remove and any access to an existing vault. Setup grants the
-Microsoft phone-provider service principal Microsoft Graph `Application.Read.All`, a tenant-wide
-application-read permission. `No` or Enter cancels deployment; sign-in and prerequisite prompts
-are separate from this approval.
-
-**Want to review without deploying?** Run interactively and choose **No** at that final prompt.
-This still performs online preflight and downloads. There is no `-DryRun` or `-WhatIf` switch;
-see [plan review](setup/docs/README.md#reviewing-the-plan-without-deploying) for details.
-
-After you approve, leave the script running while it configures the application, creates the
-Azure resources, and publishes the code. Wait for **Deployment completed** and the saved summary
-before moving to Step 4.
-
-Setup creates the Function App, storage, Key Vault, identities, and monitoring resources.
-**Do not create these resources manually first.**
-Keep the public certificate and timestamped summary in `epp-output` beside the script.
-Use the [summary field guide](setup/docs/README.md#read-the-deployment-summary) to find your
-resources, endpoint URL, application client ID, and encryption key ID. No private key is downloaded.
-
-Setup verifies that the Function can resolve its selected private-key reference before changing
-the registered encryption certificates. It also verifies **Easy Auth**, the endpoint's
-caller-authentication protection, before opening public access. Never disable it to fix a setup
-or testing problem. If setup fails, use the
-[troubleshooting guide](setup/docs/Troubleshooting.md) rather than deleting resources and starting over.
-
-To find the deployed resources, open **Azure portal > Resource groups** and select the group
-listed in your deployment summary.
-
-
-
-*This existing Premium test deployment shows the resource types to look for, not a new
-deployment or a guarantee that your resource list will be identical. Use setup rather than
-the portal's Create button to deploy the endpoint.*
-
-## 4. Connect your provider and test
-
-Complete the [provider authentication instructions](docs/ONBOARDING.md#complete-provider-authentication)
-for your selected integration. This means either entering credentials in the setup-created Key Vault
-or having the provider administrator authorize the application. Azure deployment alone does not
-complete that step. Keep credentials out of source code, screenshots, and shared logs.
-
-For an API-key integration, follow the
-[illustrated Key Vault steps](docs/ONBOARDING.md#where-to-enter-api-key-credentials-in-key-vault)
-using your own vault and the exact secret names required by your provider. OAuth integrations
-use the provider-administrator handoff instead; do not create API-key secrets for them.
-
-In your Function App, open **Settings > Authentication**. Check that authentication is
-**Enabled**, access is set to **Require authentication**, and unauthenticated requests receive
-**HTTP 401 Unauthorized**. These are checks, not instructions to change the approved caller.
-
-
-
-*The **Microsoft** identity provider on this page validates the caller. It is not the phone
-provider you selected for SMS or voice.*
-
-Ask your onboarding owner to arrange the
-[approved deployed-endpoint checks](docs/ONBOARDING.md#validate-the-deployed-endpoint):
-
-1. Confirm that missing or unauthorized caller credentials are rejected.
-2. Run an authorized **evaluation** to check authentication and decryption without a provider send.
-3. Run a separately approved **live test for each channel you intend to activate** and confirm
- both provider acceptance and receipt of the SMS or voice call. Acceptance alone is not proof of delivery.
-4. Check that the request and relevant logs appear in Application Insights.
-
-The [illustrated monitoring steps](docs/MONITORING.md#find-the-linked-resources-in-the-portal)
-show where to open Application Insights and its Logs workspace.
-
-This repository does not supply a self-service token tool for the authorized Microsoft caller.
-An ordinary Azure CLI token is not a substitute. If the approved test procedure is unavailable
-or any check fails, **do not activate the endpoint**. Do not blindly retry a timed-out live send:
-the provider may already have accepted it.
-
-## 5. Activate and hand over
-
-Have an **Authentication Policy Administrator** follow the
-[supported activation procedure](setup/docs/README.md#step-3---manually-validate-and-activate-policy).
-They must save the existing policy, apply the approved endpoint URL and application client ID,
-preserve other settings, and read back and validate the change. **Setup does not activate policy.**
-The EPP policy fields require Microsoft's assisted procedure; do not guess a Graph update.
-
-Repeat for each authentication method you intend to enable. SMS and voice can share the same
-Function endpoint URL and application client ID; no additional setup run is needed for the second channel.
-
-Before handing over, confirm:
-
-- [ ] Provider authentication, authorized testing, and recipient delivery are verified.
-- [ ] Policy is backed up, activated, and checked, with a [rollback plan](setup/docs/README.md#rollback-and-decommissioning).
-- [ ] [Monitoring and alert notifications](docs/MONITORING.md) work for your chosen runtime.
-- [ ] Owners are assigned for provider credentials, [certificate renewal](setup/docs/README.md#encryption-certificate-lifecycle),
- support, and costs.
-
-Setup creates telemetry resources, **not alert rules or renewal notifications**.
-Keep the deployment summary and validation evidence in restricted storage.
-Deleting Azure resources does not undo an authentication policy change.
-
-## Find more detail
-
-| When you need to... | Read |
-| --- | --- |
-| Check permissions, prompts, hosting plans, or renewal | [Setup reference](setup/docs/README.md) |
-| Find IDs, add provider credentials, or run acceptance checks | [Customer configuration and validation](docs/ONBOARDING.md) |
-| Resolve a setup or delivery problem | [Troubleshooting](setup/docs/Troubleshooting.md) |
-| Find logs and set up alerts | [Monitoring](docs/MONITORING.md) and [Application Insights](docs/APPLICATION-INSIGHTS.md) |
-| Choose identity and configuration alerts | [Three recommended Entra monitors](docs/SIGN-IN-MONITORING.md#recommended-monitors), with sign-in investigation and alert setup |
-| Understand the architecture, settings, or packages | [Technical reference](TECHNICAL.md) and [HTTP contract](docs/CONTRACT.md) |
-| Explore multiple Azure regions | [Manual Front Door guide](docs/FRONTDOOR.md); not included in guided setup |
-| Build a custom implementation | [JavaScript](javascript/README.md), [C#](dotnet/README.md), or [Python](python/README.md) |
-| Implement the Entra ID-facing BYOT interface | [BYOT request and response guide](docs/BRING-YOUR-OWN-PROVIDER.md); caller authentication, encrypted payload, and responses, with customer-owned service integration |
-| Design primary-to-secondary provider fallback | [Developer walkthrough](docs/MULTI-PROVIDER-IMPLEMENTATION.md); not a built-in feature |
+Keep credentials, phone data, OTPs, encrypted bodies, and raw provider responses out of issues
+and support requests. See the [complete documentation index](docs/README.md).
diff --git a/TECHNICAL.md b/TECHNICAL.md
index 1dcab2a..9cc60b3 100644
--- a/TECHNICAL.md
+++ b/TECHNICAL.md
@@ -1,35 +1,35 @@
-# External Phone Provider: Technical Reference
+# Technical reference
-For the single-region customer deployment flow, start with the
-[main onboarding guide](README.md).
+
-Azure Front Door is an [optional manual multi-region design](docs/FRONTDOOR.md), not a feature
-enabled by the single-region setup script. Its additional readiness endpoint is not shipped in
-the release packages, and regional failover does not change the SendOtp contract.
+[Documentation](docs/README.md) | [First deployment](docs/GETTING-STARTED.md) | [HTTP contract](docs/CONTRACT.md)
-Use this reference when developing or customizing the endpoint. Each language folder implements
-the same [HTTP contract](docs/CONTRACT.md), with provider-specific adapters, environment settings,
-and managed-identity access to credentials.
+This reference covers implementation details, app settings, and packages.
+Sections: [implementations](#implementations), [guided setup](#guided-epp-setup),
+[download](#download-a-function-zip), [build](#build-zips-locally),
+[settings](#configure-environment-variables), [provider details](#provider-specific-integration-details),
+or [security](#security). For regional continuity, see
+[resiliency and business continuity](#resiliency-and-business-continuity).
## Single-region architecture
-
+See the [single-region diagram in the sample overview](README.md#architecture).
-Microsoft Entra ID sends an authenticated, encrypted request. Easy Auth checks the caller,
-then the Function decrypts the request using its Key Vault-backed key. For a live request, the
-selected adapter submits the message and returns success after provider acceptance, not after
-confirmed recipient delivery. Application Insights collects the available request and log telemetry.
+Microsoft Entra ID sends an authenticated, encrypted request. Easy Auth checks the caller, the
+Function decrypts the request, and the selected adapter submits a live message. Success means
+provider acceptance, not confirmed delivery.
-The diagram is an overview, not a portal screenshot. East US is illustrative; select a region
-with capacity and quota for your hosting plan. The provider-secret path applies to API-key
-integrations; OAuth integrations use managed-identity token exchange instead. Credential caching
-depends on the [selected cache settings](docs/CONTRACT.md#credential-caching-and-refresh), and
-[log formats vary by runtime](docs/MONITORING.md#runtime-specific-log-discovery).
+East US is illustrative. API-key integrations use provider secrets; OAuth integrations use
+managed-identity token exchange. See [credential caching](docs/CONTRACT.md#credential-caching-and-refresh)
+and [runtime logs](docs/MONITORING.md#runtime-specific-log-discovery).
-An authorized, valid encrypted **evaluation request (`mode: 2`)** returns the matching nonce
-without submitting a message to the provider. When caching is enabled, credential preparation
-can run independently; it never sends an OTP. Authentication failures can return **401 or 403**,
-neither of which is a successful evaluation.
+An authorized **evaluation request (`mode: 2`)** returns the nonce without sending a message.
+Authentication failures return **401 or 403**.
+
+## Resiliency and business continuity
+
+The [Azure Front Door design](docs/FRONTDOOR.md) adds regional origins behind one public URL.
+It covers capacity, shared keys, dependencies, readiness, failover drills, and rollback.
## Implementations
@@ -39,51 +39,26 @@ neither of which is a successful evaluation.
| C# (.NET isolated worker) | Available | [dotnet/](dotnet/) |
| Python (v2 model) | Available | [python/](python/) |
-All implementations conform to the **language-agnostic contract** in
-[docs/CONTRACT.md](docs/CONTRACT.md): shared HTTP intent, config/env var names, Key Vault secret
-names, and fail-closed/managed-identity/privacy boundaries. Internal provider APIs, binding errors,
-credential caching, and telemetry differ by runtime; consult the documented differences rather
-than assuming identical internals.
+Choose **one language and one provider per Function App**. All implementations share the
+[HTTP, configuration, and trust contract](docs/CONTRACT.md); runtime behavior can differ.
-Choose one language and configure the adapter for your provider. No provider is preferred or selected
-by default. Deploy each language separately, not all three to the same Function App. See the
-[shared configuration](docs/CONTRACT.md#default-provider-and-configuration-readers).
+For local work, use the selected runtime guide. For a custom integration, start with
+[Bring your own provider](docs/BRING-YOUR-OWN-PROVIDER.md).
-For local development, manual configuration, and deployment checks, see the
-[detailed configuration and validation guide](docs/ONBOARDING.md).
+## Guided EPP setup
-For the provider-neutral inbound interface, read the
-[BYOT request and response guide](docs/BRING-YOUR-OWN-PROVIDER.md). It covers the Entra ID envelope,
-caller authentication, encrypted delivery context, and endpoint responses. The outbound service
-integration remains customer-owned; the guide does not add an adapter or guided setup support.
+Follow the [five-step guide](docs/GETTING-STARTED.md). `Setup-Epp.ps1` retrieves its support files
+from one commit and deploys after plan approval. See the [setup reference](setup/docs/README.md#step-2---download-and-run-one-script).
-## Guided EPP setup
+Review [application permissions](setup/docs/README.md#step-1---manually-create-the-application)
+and [certificate selection](setup/docs/README.md#encryption-certificate-selection). Setup pins one
+PEM secret version and downloads only the public certificate.
-Use **[setup](setup/docs/README.md)** for **Step 2: endpoint deployment**. Download only
-`Setup-Epp.ps1`; it downloads its supporting PowerShell, Bicep, package catalog, and provider JSON
-from the same commit. Customers select a language, provider, Global or EU endpoint,
-a [service plan](setup/docs/README.md#service-plan-selection), and a resource prefix, then approve
-one complete plan. Manual Step 1 only creates the dedicated app
-registration; PowerShell configures its service principals, `Epp.Invoke`, Microsoft caller access,
-Graph `Application.Read.All`, the provider-tenant allowlist preview, encryption certificate, and
-Easy Auth. Read-only preflight discovers existing `AsymmetricX509Cert` / `Encrypt` credentials.
-Customers choose a matching Key Vault certificate version to reuse, with explicit cleanup of any
-other encryption credentials, or replacement with a new certificate. The approved deployment
-stages the private-key reference, verifies its resolution by the Function system identity, and
-registers exactly one public encryption certificate while preserving non-encryption credentials.
-Setup downloads only the public certificate and pins the Function to its PEM secret version.
-Reuse can reference another vault in the same tenant; provider credentials remain in the
-setup-created vault. Replacement is coordinated maintenance, not zero-downtime rotation.
-Each deployment accepts both SMS and voice. The request's `channel` selects its configured
-provider endpoint; the two URLs can be identical or different.
-Certificate synchronization and policy activation remain manual. The home tenant remains allowed by Entra.
+One deployment accepts SMS and voice; each request's `channel` selects its configured provider URL.
## Download a Function ZIP
-**Optional developer/manual deployment path:** guided setup already selects, verifies, builds as
-needed, and publishes the package. Customers using it should continue with
-[provider authentication and validation](docs/ONBOARDING.md#complete-provider-authentication),
-not deploy a second ZIP.
+Guided setup already publishes the package. Use this section for manual deployment.
Download the latest successful CI ZIP for your chosen language:
@@ -93,26 +68,15 @@ Download the latest successful CI ZIP for your chosen language:
| .NET | [epp-dotnet-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest/download/epp-dotnet-source.zip) | C# Function source and project file; build/publish before deployment |
| Python | [epp-python-source.zip](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest/download/epp-python-source.zip) | Source for Azure remote build on Linux |
-Customers do not need PowerShell or a local build toolchain to download these files. Verify downloads
-against the corresponding release's `SHA256SUMS.txt`. Configure the target Function App's runtime, app settings,
-Key Vault access, and Easy Auth before deploying. .NET requires building/publishing the extracted
-project; Python requires remote build to install dependencies. Neither source ZIP can run directly
-as a run-from-package artifact. GitHub's **Code > Download ZIP**
-is the whole source repository, not a Function deployment package.
-
-Each successful `main` build tests all three implementations, builds and inspects the ZIPs, and
-publishes a new versioned release marked as the latest release.
-The direct links above and guided setup therefore track the newest successful CI package build. Get builds from
-[Latest release](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest).
-Older releases remain available; existing assets are not overwritten. Pull requests build downloadable
-workflow artifacts only and cannot publish releases. GitHub sign-in may be required for workflow
-artifacts, but public release downloads do not require a local build. Packaging does not deploy or
-verify live provider delivery.
+Verify `SHA256SUMS.txt`, configure the runtime, settings, Key Vault, and **Easy Auth**, then build
+.NET or use Python remote build. Source ZIPs are not run-from-package artifacts.
+
+Successful `main` builds publish a versioned [release](https://github.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/releases/latest).
+PR builds publish workflow artifacts only. Packaging does not deploy or verify delivery.
## Build ZIPs Locally
-For custom builds, run the script for your chosen language from the repository root. These standalone scripts create
-ZIPs locally; they do not sign in to Azure, upload code, or change app settings.
+Run one packaging script from the repository root. It creates a local ZIP without changing Azure.
| Language | Root-level script | Prerequisites | ZIP in `artifacts/` |
|---|---|---|---|
@@ -126,28 +90,19 @@ pwsh -File ./package-dotnet.ps1
pwsh -File ./package-python.ps1
```
-Choose one command; each packages only its language. The scripts locate source relative to their
-own location, so invoking an absolute script path also works from another directory. Each ZIP has
-`host.json` at its root, with no enclosing language folder. Local settings, credential files, and
-first-party tests are excluded. Generated ZIPs are ignored by Git.
-
-JavaScript installs production dependencies from the lockfile in a temporary folder; your working
-`node_modules` is not copied or modified. Dependency lifecycle scripts are disabled for this sample's
-JavaScript dependencies. If you add native dependencies or packages requiring install scripts,
-review packaging and build them for the target Azure OS.
-
-**.NET is a source ZIP, not compiled output.** It contains `dotnet.csproj`, `host.json`, `Program.cs`,
-and the C# files under `Functions/` and `Src/`. Packaging does not run restore, build, or publish,
-and needs no .NET SDK. It excludes `bin/`, `obj/`, tests, local settings, and compiled dependencies.
-Customers extract it and run `dotnet publish dotnet.csproj --configuration Release --output ../publish`
-with the .NET 8 SDK, or use a deployment pipeline that builds the project. Deploy the resulting
-publish output with `host.json` at its root, not the source ZIP directly. CI tests this customer
-build from an extracted copy; that temporary publish output is not included in the download.
-
-**Python is a source ZIP, not a ready-to-run package.** Deploy to a Linux Function App with remote
-build enabled in the deployment tool for your hosting plan, so Azure installs `requirements.txt`.
-Do not use this source ZIP directly with run-from-package or copy Windows-installed Python dependencies
-to Azure. The script deliberately does not invoke pip or include a local virtual environment.
+Choose one command. Each ZIP has `host.json` at its root and excludes local settings, credentials,
+and tests.
+
+JavaScript installs production dependencies from the lockfile in a temporary folder. Review
+packaging for native dependencies or required lifecycle scripts. It does not modify `node_modules`.
+
+**The .NET download contains source.** Run
+`dotnet publish dotnet.csproj --configuration Release --output ../publish` with .NET 8, then deploy
+the publish output with `host.json` at its root. The archive includes `dotnet.csproj`,
+`Program.cs`, `Functions/`, and `Src/`, and excludes `bin/` and `obj/`.
+
+**The Python download contains source.** Deploy it to Linux with remote build so Azure installs
+`requirements.txt`.
Existing archives are never overwritten. For another build, specify a new path:
@@ -155,11 +110,8 @@ Existing archives are never overwritten. For another build, specify a new path:
pwsh -File ./package-javascript.ps1 -OutputPath ./artifacts/epp-javascript-v2.zip
```
-The same `-OutputPath` option works for all three scripts. Configure the destination app's runtime,
-app settings, Key Vault access, and Easy Auth separately before deployment. See
-[deployment and validation](docs/ONBOARDING.md#4-package-deploy-and-verify). Packaging success does
-not verify cloud configuration or provider delivery. File selection is tailored to this sample;
-extend it deliberately if you add runtime assets, and never put secrets in application source.
+`-OutputPath` works for all three scripts. Configure the destination and follow
+[deployment and validation](docs/ONBOARDING.md#4-package-deploy-and-verify). Never include secrets.
## The design in one line
@@ -169,32 +121,23 @@ Only provider acceptance returns the nonce for live requests. Incoming `mode: 2`
non-delivery path: after platform authentication, validate and decrypt, then echo the nonce without
calling a provider.
-See [docs/CONTRACT.md](docs/CONTRACT.md) for the full specification every implementation follows.
-
Request `tenantId`, `channel`, `mode` and `ttlSeconds` are request data, not extra environment settings.
The trusted tenant issuer, endpoint-app audience and authorized Microsoft caller are configured in Easy Auth,
not in application environment settings or incoming request data.
## Configure environment variables
-For guided deployment, use the [customer values and ownership table](docs/ONBOARDING.md#values-and-ownership)
-to inspect what setup already configured. The following sample/local instructions are for
-developers and manual deployments; they are not an extra guided-setup step.
+For guided deployment, inspect the [values and ownership table](docs/ONBOARDING.md#values-and-ownership).
+The following settings are for local or manual deployment.
-Use the [sample settings](docs/local.settings.sample.json) as the starting point for the chosen
-runtime. All entries in its `Values` object are **strings**. The application reads environment
-variables; Azure Functions Core Tools loads that `Values` object for local runs.
+Start from [sample settings](docs/local.settings.sample.json). Every `Values` entry is a string.
-The sample uses `node`; change it to `python` or `dotnet-isolated` for those runtimes. Replace the
-provider, endpoint, vault and test-key placeholders before use. Its storage value assumes **Azurite
-is running**; do not copy `UseDevelopmentStorage=true` into Azure. Optional settings stay in the table
-below rather than appearing as required placeholders in the sample. Keep explanatory comments outside
-`Values`, otherwise the host loads them as environment variables too.
+Change `node` to `python` or `dotnet-isolated` as needed. Replace placeholders. The sample storage
+value requires **Azurite** and must not be copied to Azure.
-The local settings file is an environment-variable input for the Functions host, **not a serialized
-`AppConfig` or request model**. For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in
-JavaScript, `config.provider_name` in Python, and `config.ProviderName` in .NET. The refactor changed
-how code accesses configuration; request fields remain separate from deployment settings.
+Local settings supply environment variables, **not a serialized `AppConfig` or request model**.
+For example, `EPP_PROVIDER_NAME` becomes `config.providerName` in JavaScript,
+`config.provider_name` in Python, and `config.ProviderName` in .NET.
| Variable | When needed | Value |
|---|---|---|
@@ -237,18 +180,14 @@ login; ordinary local machines have no managed-identity endpoint. Use offline te
evaluation locally, or an explicitly injected test resolver for integration work. Never commit local
settings, keys or test credentials.
-When their selected cache is enabled, configured providers are
-[prepared per worker](docs/CONTRACT.md#credential-caching-and-refresh).
-JavaScript/Python warm credentials and poll for refresh; .NET warms at startup and retrieves
-replacements on cache misses, without a periodic poller. Credential acquisition never sends an OTP.
-Evaluation skips provider work, but configured workers can independently acquire credentials at
-startup. Disabling the selected cache skips startup preparation, polling, and cross-request reuse,
-but live requests still acquire credentials. Each switch accepts trimmed, case-insensitive `true` or
-`false`; an invalid selected value fails live credential resolution closed without preventing evaluation.
-The other provider-auth mode's switch is ignored. Setup writes both as `false` for FC1 or `true` for
-EP1; older packages that do not read these settings still require a supporting release. Restart after
-changes. Leave `EPP_PROVIDER_NAME` unset or disable its cache for local evaluation-only work without
-credential acquisition. Platform-managed identity caching and decryption-key references are separate.
+For cache values, failure behavior, and runtime differences, use the
+[credential caching contract](docs/CONTRACT.md#credential-caching-and-refresh).
+JavaScript/Python poll; .NET refreshes on cache misses. Setup writes both switches as `false` for FC1
+or `true` for EP1; deploy a supporting package and restart after changes.
+Evaluation skips provider work, but cached workers can independently acquire credentials without
+sending an OTP. Leave `EPP_PROVIDER_NAME` unset or disable its cache for local evaluation without
+credential acquisition. Provider caching is separate from platform-managed identity caching and
+decryption-key references.
Core Tools does not resolve Azure Key Vault reference expressions locally. Supply the local test PEM
or base64 PEM directly; use a reference such as `@Microsoft.KeyVault(SecretUri=https://.vault.azure.net/secrets//)`
@@ -293,9 +232,9 @@ For an SMS request, the adapter builds the following JSON from the decrypted del
```
`phoneNumber` must match `^\+[1-9][0-9]{1,14}$`; the leading `+` is preserved. SMS passes the complete
-`message` unchanged as `message.text`, including whitespace. For Voice, each six-digit numeric run
-that is not part of a longer number is rendered with comma-separated digits, and the complete paced
-message is sent twice with one separating space. Telesign performs text-to-speech for Voice; no
+`message` unchanged as `message.text`, including whitespace. For Voice, each standalone six-digit
+number is rendered with comma-separated digits. The complete paced message is sent twice with one
+separating space. Telesign performs text-to-speech for Voice; no
separate speech object is needed.
A nonblank string `locale` becomes `message.language`; otherwise language is omitted. The envelope
channel selects the single `sms` or `voice` entry. `correlation_id` uses a nonempty string request
@@ -309,11 +248,13 @@ omits that header on live sends and does not forward it from incoming requests.
`mode: 2` evaluation path for Function tests without delivery: it skips provider HTTP and credential
lookup entirely, rather than invoking Telesign shutter mode.
-Responses normalize `reference_id` and `status.code`/`status.description` internally; provider
-metadata is not exposed in the public nonce response. [Application logs](docs/CONTRACT.md#application-logs)
-include only the provider HTTP status, mapped status/outcome and the bounded raw provider reference
-ID as `providerMessageId` for support lookup, never the raw response or description. Existing numeric success codes
-are retained (SMS: 200, 203, 290-292; Voice: 100-103). EPP code `3001` ("Message in progress"),
+Responses normalize `reference_id` and `status.code`/`status.description` internally. Provider
+metadata is not exposed in the public nonce response. [Application logs](docs/LOGGING.md#safe-fields-and-identifiers)
+include the provider HTTP status and mapped status/outcome. They also include a length-limited
+provider reference ID as `providerMessageId` for support lookup. They exclude the raw response
+and description.
+Existing numeric success codes remain unchanged (SMS: 200, 203, 290-292; Voice: 100-103).
+EPP code `3001` ("Message in progress"),
observed for both channels, is also accepted on successful HTTP responses. This acknowledges
provider acceptance, not handset receipt or completed audio playback. The sample's recognized
codes are not a complete provider status-code catalog. Missing, malformed, or
@@ -322,33 +263,20 @@ account access with Telesign before production.
## Security
-**Easy Auth (App Service Authentication) is the only caller-authentication gate, before the anonymous
-Function.** Enable it with `requireAuthentication=true`, `unauthenticatedClientAction=Return401` and
-`requireHttps=true`. Configure the trusted tenant issuer and `allowedAudiences` for the endpoint app,
-and a **nonempty `allowedApplications`** list pinned to the authorized Microsoft caller application ID.
-Do not exclude the SendOtp path. The handler does not parse or validate bearer tokens, and there is
-no backup application validation or function-key gate. **Never expose this endpoint to the public
-internet with Easy Auth disabled or bypassed.** See [platform setup](docs/ONBOARDING.md#2-provision-encryption-and-deployment-trust).
-
-JWE decryption protects the payload but **does not authenticate the caller**: anyone with the public key can
-encrypt a request. A nonce echo, including a fixed nonce, is not caller authentication. Provider API
-keys are read from **Key Vault** via **managed identity**; they authenticate the outbound provider call,
-not the inbound request.
+**Easy Auth is the only caller-authentication gate before the anonymous Function.**
+Follow the [exact trust checklist](docs/ONBOARDING.md#manual-deployment-trust-checklist) for HTTPS,
+issuer, audience, caller allowlist, and unauthenticated rejection. Never disable it or exempt SendOtp;
+there is no handler token validator or function-key fallback.
-Core Tools does not provide Easy Auth. Local execution is unauthenticated: bind only to loopback,
-with no tunnels or public forwarding. Offline tests cover application behavior, not platform
-authentication; [separate deployed security checks](docs/ONBOARDING.md#4-package-deploy-and-verify) are required.
+JWE and its nonce prove no caller identity: anyone with the public key can encrypt.
+Provider credentials authenticate outbound calls, not incoming requests; vault access uses managed identity.
+Core Tools has no Easy Auth: bind locally to loopback without tunnels.
+[Deployed checks](docs/ONBOARDING.md#validate-the-deployed-endpoint) remain required after offline tests.
## Docs
-- **[README.md](README.md)**: single-region customer onboarding.
-- **[docs/ONBOARDING.md](docs/ONBOARDING.md)**: customer value sources, provider handoffs, validation, and optional manual deployment.
-- **[docs/FRONTDOOR.md](docs/FRONTDOOR.md)**: optional manual multi-region onboarding and observed failover limitations.
-- **[docs/CONTRACT.md](docs/CONTRACT.md)**: the language-agnostic contract every implementation follows.
-- **[Application logs](docs/CONTRACT.md#application-logs)**: separate service and completion events,
- and the meaning of Microsoft, Function and provider identifier fields.
-- **[Application Insights](docs/APPLICATION-INSIGHTS.md)** and **[monitoring](docs/MONITORING.md)**:
- collection, runtime-aware queries, alert setup, and ongoing operations.
+Use the [documentation index](docs/README.md) for setup, troubleshooting, operations, and advanced designs.
+For code work, keep the [HTTP contract](docs/CONTRACT.md) and [log schema](docs/LOGGING.md) together.
## Contributing a language or provider
diff --git a/docs/APPLICATION-INSIGHTS.md b/docs/APPLICATION-INSIGHTS.md
index 514fb77..8ed5f79 100644
--- a/docs/APPLICATION-INSIGHTS.md
+++ b/docs/APPLICATION-INSIGHTS.md
@@ -1,54 +1,37 @@
-# Application Insights for the External Phone Provider
+# Application Insights
-Application Insights helps you see which requests ran, how long they took, and what the Function
-reported along the way. It does **not** confirm that an SMS or voice call reached the recipient.
+
-**Looking for your logs?** Start with the [monitoring guide](MONITORING.md). It shows how to find
-the resources from your deployment summary, run queries, and create alerts.
+[Documentation](README.md) | [Monitoring walkthrough](MONITORING.md) | [Log schema](LOGGING.md)
-For user authentication outcomes, see [Microsoft Entra sign-in monitoring](SIGN-IN-MONITORING.md).
-It uses a separate tenant log-export pipeline, not Function telemetry; neither source alone
-proves phone delivery.
+Use the [monitoring walkthrough](MONITORING.md) for queries and alerts. Use
+[single-region configuration](#single-region-configuration) for missing telemetry.
-This reference explains how collection works for **JavaScript, .NET isolated, and Python**,
-what each signal means, and why data may be missing. Start with
-[single-region configuration](#single-region-configuration) to check a guided deployment;
-use the remaining sections for runtime differences or a multi-region design.
-You do not need Azure Front Door for Function telemetry.
+Function telemetry covers execution, timing, and logs. Provider delivery evidence and
+[Microsoft Entra ID sign-in monitoring](SIGN-IN-MONITORING.md) are separate.
-These instructions do not deploy resources or enable extra instrumentation, alerts, or availability
-tests. Use the [setup guide](../setup/docs/README.md) for deployment and the
-[approved test handoff](ONBOARDING.md#validate-the-deployed-endpoint) to generate test traffic.
+Sections: [collection pipelines](#how-telemetry-reaches-application-insights),
+[signals and tables](#signals-tables-and-metrics-are-different-things),
+[single-region checks](#single-region-configuration), [ingestion identity](#identity-and-ingestion),
+[multi-region layout](#multi-region-configuration), [coverage queries](#read-only-queries-to-establish-what-is-present),
+or [sampling/privacy limits](#sampling-privacy-and-collection-limits).
## How telemetry reaches Application Insights
-There are two execution processes to consider: the **Functions host**, which dispatches
-invocations, and the **language worker**, which runs the endpoint code.
-
-1. The host records invocation telemetry. With the standard Functions logging pipeline,
- application logs are forwarded from the worker to the host for export.
-2. A worker can instead have its own SDK or OpenTelemetry instrumentation and exporter.
- That pipeline requires language-specific initialization, authentication, filtering, and
- sampling; configuring the host alone does not configure every worker signal.
-3. The exporter sends telemetry to the Application Insights ingestion endpoint. The
- Application Insights resource identifies the application and its linked Log Analytics
- workspace stores the log records.
-4. Application Insights views and Azure Monitor Logs queries use those records. Correlation
- identifiers can connect requests, dependencies, and logs when instrumentation propagates
- the context. Background work does not necessarily belong to a request.
-
-In OpenTelemetry terminology, a distributed trace contains spans. In Application Insights,
-server spans generally appear as **requests**, client spans as **dependencies**, and
-application log messages as **traces**. The `traces` table is not a list of all distributed
-tracing spans.
+The **Functions host** records invocation telemetry; the **language worker** runs endpoint code.
+A direct worker exporter needs separate initialization, authentication, filtering, and sampling.
+
+Application Insights sends records to its linked Log Analytics workspace. Correlation context
+connects requests, dependencies, and logs.
+
+Application Insights generally stores server spans as **requests**, client spans as
+**dependencies**, and log messages as **traces**.
### What the checked-in sample configures
-The [deployment template](../setup/infra/resources.bicep) defines a workspace-based Application
-Insights resource, the Function's connection and authentication settings, and a role assignment
-for ingestion. It also defines a **separate** Function diagnostic setting that sends `allLogs`
-and `AllMetrics` to the workspace. Diagnostic settings do not install worker instrumentation
-and do not mean every Azure resource or every dependency is being observed.
+The [deployment template](../setup/infra/resources.bicep) configures Application Insights,
+authenticated ingestion, and a separate `allLogs`/`AllMetrics` diagnostic setting. Diagnostic
+settings do not instrument every worker or dependency.
| Implementation | Checked-in behavior | What not to assume |
| --- | --- | --- |
@@ -56,26 +39,18 @@ and do not mean every Azure resource or every dependency is being observed.
| [JavaScript](../javascript/host.json) | Uses standard host Application Insights settings with sampling enabled and `Request` excluded from sampling. | The [package manifest](../javascript/package.json) does not include a direct Application Insights/OpenTelemetry exporter. Host integration is not proof that all outbound Node.js HTTP calls are traced. |
| [Python](../python/host.json) | Uses standard host Application Insights settings with sampling enabled and `Request` excluded from sampling. | The [requirements](../python/requirements.txt) do not include an Azure Monitor/OpenTelemetry exporter. A Python log message or `requests` call is not automatically a structured custom event or a dependency span. |
-These are source defaults, not an inventory of a running deployment. Deployed package versions,
-application-setting overrides, runtime versions, and log filters can change the result. The
-guided setup can publish a released package rather than the current source.
+These are source defaults. Packages, settings, runtime versions, and filters can change them.
-The endpoint emits application events such as `request_received`, `provider_response_processed`,
-`request_failed`, `request_completed`, and `credential_refresh_failed` through language logging
-APIs. These are **log events**, not a promise of rows in `customEvents`. .NET uses structured
-`ILogger` events, JavaScript uses its logging helper, and Python uses logging records with extra
-fields. Message formatting, property names/casing, and preservation of extra fields depend on
-the logging/export path. Inspect the actual schema before sharing one event-field query across
-languages.
+[Application events](LOGGING.md#event-sequence) are **logs**, not guaranteed `customEvents`.
+Use [runtime-specific discovery](MONITORING.md#runtime-specific-log-discovery) before sharing queries:
+exporters can change formatting, casing, and which properties survive.
-When adding direct worker instrumentation in a future code change, follow the
+For direct worker instrumentation, follow the
[Functions OpenTelemetry guidance](https://learn.microsoft.com/en-us/azure/azure-functions/opentelemetry-howto)
-for the selected language. Initialize the exporter and required instrumentations, configure
-its credential, and verify correlation and duplicate suppression. Do not simply copy another
-language's host settings. Other Azure Functions languages are outside this repository's sample;
-use the corresponding language tab in the Microsoft guidance for their support requirements.
+for the selected language and verify correlation and duplicate suppression.
-## Signals, tables, and metrics are different things
+
+## Telemetry signals and tables
The following table maps **Application Insights Logs** names to **Log Analytics workspace**
names. Table availability and contents depend on what was actually emitted and ingested.
@@ -98,7 +73,7 @@ different sampling behavior and dimensions; a chart and a raw row count need not
**Azure resource metrics** come from the platform and are queried under the relevant resource
in Azure Monitor Metrics. Function execution/instance/memory metrics depend on the hosting plan;
FC1 and EP1 do not expose identical sets. Key Vault has its own request/latency signals, Storage
-has transaction/availability/latency signals, and optional Front Door has edge/origin signals.
+has transaction/availability/latency signals, and Front Door adds edge/origin signals.
These are not automatically Application Insights dependencies or custom metrics.
Resource logs are another independent source. Depending on the resource and diagnostic setting's
@@ -135,8 +110,9 @@ Review the following in the portal without assuming that resource creation prove
controlled live test. Evaluation skips the provider send; it cannot validate that dependency
path. Background credential refresh may run independently during either test.
-Do not weaken Easy Auth or send periodic OTPs just to populate telemetry. A generic unauthenticated
-URL probe does not validate the endpoint's authenticated, encrypted EPP contract.
+Keep Easy Auth and the approved delivery flow intact while validating telemetry. Use an authorized
+evaluation request for the authenticated, encrypted EPP path rather than generating periodic OTPs
+or relying on a generic unauthenticated URL probe.
### Identity and ingestion
@@ -184,20 +160,17 @@ proof of the Function's execution region. `AppRoleInstance` identifies an instan
regional label. In a shared component, use verified role naming or an explicitly emitted region
property; do not expect Azure resource tags to become telemetry properties automatically.
-For separate workspaces, use
-[cross-workspace queries](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cross-workspace-query)
-with explicit `workspace("").AppRequests` references and a region label
-for each source before the union. The placeholders must be replaced by authorized operators,
-and the query identity needs access to all sources. Group by origin before aggregating so a
-healthy region cannot hide a failing one. Check each region's latest telemetry, including idle
-or standby regions; no traffic and broken collection can both look like an empty result.
+Use the [cross-workspace queries and missing-origin check](MONITORING.md#3-multiple-regions-and-cross-workspace-queries)
+with explicit sources, authorized access to each, and per-origin grouping. Check idle/standby
+origins too: no traffic and broken collection can both look empty.
-If Front Door is used, inspect its platform metrics and diagnostic logs separately. An edge or
-WAF rejection, origin-connectivity failure, or health-probe result is not necessarily a Function
-request. Front Door origin health is not evidence of provider authentication or message delivery.
-The [optional Front Door guide](FRONTDOOR.md) describes that topology.
+For the [Front Door multi-region design](FRONTDOOR.md), inspect
+[edge metrics and logs separately](MONITORING.md#8-optional-azure-front-door-layer).
+Edge/WAF rejections, origin-connectivity failures, and probes need not appear as Function requests.
+Origin health does not prove provider authentication or delivery.
-## Read-only queries to establish what is present
+
+## Telemetry coverage queries
Run these examples from the **linked Log Analytics workspace's Logs** view. They use workspace
table/column names (`TimeGenerated`, `AppRoleName`, `OperationId`, `Properties`), not the
diff --git a/docs/BRING-YOUR-OWN-PROVIDER.md b/docs/BRING-YOUR-OWN-PROVIDER.md
index 7f6fb69..67051f5 100644
--- a/docs/BRING-YOUR-OWN-PROVIDER.md
+++ b/docs/BRING-YOUR-OWN-PROVIDER.md
@@ -1,34 +1,32 @@
-# BYOT integration: the Entra ID request and response
+# Bring your own provider
-**Microsoft Entra ID** sends your **External Phone Provider (EPP)** endpoint
-an encrypted phone number and one-time passcode message. Your endpoint verifies the caller,
-decrypts the details, and either evaluates the request without sending or uses your phone service.
-It then responds to Entra ID. **Your team owns the connection to that service.**
+
-Before deploying, ask your tenant administrator to confirm
-[eligibility, testing, and activation with Microsoft](ONBOARDING.md#before-purchasing-or-deploying).
-Deploying the endpoint alone does not enable tenant policy.
+[Documentation](README.md) | [Exact HTTP contract](CONTRACT.md) | [Runtime guides](README.md#develop)
+
+Connect a phone service to the **External Phone Provider (EPP)** contract. **Microsoft Entra ID**
+sends an encrypted delivery request; the endpoint verifies, decrypts, evaluates, or sends it.
+
+Confirm [eligibility, testing, and activation](ONBOARDING.md#before-purchasing-or-deploying)
+before deployment.
+
+Follow [caller verification](#1-verify-the-caller), [decryption](#2-read-and-decrypt-the-request),
+[evaluation/live handling](#3-evaluate-or-send), and [responses](#4-respond-to-entra-id).
+Then [register your integration](#where-to-add-your-integration) and [validate before activation](#before-activation).
## 1. Verify the caller
-Entra ID sends **`POST /api/SendOtp` over HTTPS**, with a signed bearer token in `Authorization`.
-In this sample, **Easy Auth** (Azure App Service Authentication) checks the token and authorizes
-the caller before the Function runs. The Function's trigger is anonymous and has no backup
-authentication check, so do not expose it with Easy Auth disabled or bypassed.
+Entra ID sends **`POST /api/SendOtp` over HTTPS** with a signed bearer token. **Easy Auth**
+authorizes the caller before the anonymous Function trigger runs.
-Require HTTPS and authentication, return `401` for unauthenticated requests, and restrict access
-to the trusted tenant issuer, endpoint-app audience, and authorized Microsoft caller application ID.
-Your Azure operator should use the endpoint app registration and
-[approved setup configuration](../setup/support/Epp.Setup.psm1) for these values, then verify
-**Function App > Authentication**. Follow the
-[trust checklist](ONBOARDING.md#manual-deployment-trust-checklist) for exact settings,
-caller-ID guidance, and token-version details.
+Require HTTPS, return `401` for unauthenticated requests, and restrict access to the trusted
+issuer, audience, and Microsoft caller. Follow the
+[trust checklist](ONBOARDING.md#manual-deployment-trust-checklist).
-**Encryption is not caller authentication.** Anyone with the public key can create a JWE
-(JSON Web Encryption) payload. Neither its nonce, the body's `tenantId`, nor tracing or
-forwarded principal headers prove that Entra ID sent it.
+**Encryption is not caller authentication.** A JWE, nonce, `tenantId`, or tracing header does
+not prove that Entra ID sent the request.
## 2. Read and decrypt the request
@@ -44,8 +42,8 @@ The JSON body is an envelope with these fields. Use the forms below across all s
| `correlationId` | No | Nonempty string for sign-in tracing. |
| `ttlSeconds` | No | Positive JSON integer up to `2147483647`, or omit it. Use `60`, not `"60"`, `60.0`, `6e1`, or null. |
-Channel and mode names are case-insensitive. Reject unsupported versions, channels, or modes.
-TTL is only checked as a number; it does not enforce passcode expiry or a delivery deadline.
+Channel and mode names are case-insensitive. TTL validation does not enforce passcode expiry
+or a delivery deadline.
@@ -54,15 +52,11 @@ for content encryption. Accept exactly five nonempty compact-JWE segments, at mo
characters**. Pass the original JWE, including its protected-header bytes, to a JOSE library.
Verify the authentication tag before using the decrypted UTF-8 JSON.
-The sample decrypts in-process with one RSA private key in `EPP_DECRYPTION_KEY_PEM`: a PEM key
-or certificate bundle containing it, as text or base64-encoded PEM. In Azure, use a
-managed-identity-resolved Key Vault secret reference. The matching public encryption credential
-belongs to the endpoint app registration.
+The sample uses one RSA private key in `EPP_DECRYPTION_KEY_PEM` as text or base64 PEM. In Azure,
+use a managed-identity-resolved Key Vault reference.
-The header's `kid` does not select a key in this sample. `EPP_ENCRYPTION_KEY_ID` only enables
-a warning on mismatch after successful decryption. Setup pins one certificate's secret version;
-renewal needs coordination because Key Vault renewal alone does not update the app registration
-or pinned reference. See the [certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle).
+The JWE `kid` does not select a key. `EPP_ENCRYPTION_KEY_ID` only warns after decryption.
+See the [certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle).
The decrypted delivery context contains:
@@ -76,31 +70,27 @@ The decrypted delivery context contains:
| `riskContext` | No | Contextual JSON data; no shared schema validation, risk evaluation, or forwarding. |
| `textToVoice` | No | Legacy speech data; no portable schema or shared forwarding. |
-Do not assume an optional field is supported just because it appears in the request.
+Treat optional fields as available only when the selected integration supports them.
For parser and wrong-type handling, see [runtime binding differences](CONTRACT.md#runtime-binding-differences).
## 3. Evaluate or send
-**Evaluation (`mode: 2` or `"evaluation"`):** authenticate, validate, and decrypt, then return
-the success response below **without sending anything**. This request path skips service
-selection, credential lookup, and outbound HTTP; no service configuration is needed.
-Configured workers may independently prepare or refresh credentials, but that does not send a message.
+**Evaluation (`mode: 2` or `"evaluation"`):** authenticate, validate, decrypt, and return success
+without service selection, credential lookup, outbound HTTP, or delivery.
-**Live (`mode: 1` or `"live"`):** your team implements and maintains the service integration.
-Map the requested SMS or voice delivery to your service, preserving the destination, message,
-and passcode. Handle voice rendering and locales as needed. Authenticate with your own service
-credentials; **never forward the incoming bearer token**.
+**Live (`mode: 1` or `"live"`):** map SMS or voice to your service while preserving the
+destination, message, and passcode. **Never forward the incoming bearer token**.
-Wait for recognized acceptance before returning success. A queued or pending result counts
-only if your integration recognizes it as acceptance. Unknown or incomplete responses must fail,
-and a success-looking body on a failed HTTP response is not acceptance. Return the result through
-the shared response behavior below, not as raw service JSON.
+Return success after the integration recognizes provider acceptance. A queued or pending result
+counts only when the integration defines it as accepted. Map unknown, incomplete, or failed HTTP
+responses to an error, even when the response body looks successful. Use the shared response
+behavior below instead of returning raw service JSON.
-The sample uses one configured service per deployment. A custom integration does not add
-guided setup support. Follow [Where to add your integration](#where-to-add-your-integration)
-for the files to create, registration, settings, and packaging.
+The sample uses one configured service per deployment. Follow
+[Where to add your integration](#where-to-add-your-integration) for the source files,
+registration, settings, packaging, and custom deployment steps.
## 4. Respond to Entra ID
@@ -172,7 +162,8 @@ or failure. Keep credential caching in the existing services:
credential specification; C# calls `FetchCredentialsAsync`. Do not add a separate credential cache
or send-retry loop. Keep service secrets in Key Vault, not in settings or source.
-### Configure, test, and package
+
+### Implementation steps
1. Start from [local.settings.sample.json](local.settings.sample.json). Copy it to an untracked
`local.settings.json` beside your runtime's `host.json`, keep all `Values` entries as strings,
@@ -193,7 +184,7 @@ or send-retry loop. Keep service secrets in Key Vault, not in settings or source
release. C# needs publish output; Python needs the documented Linux remote build.
Follow [manual deployment](ONBOARDING.md#optional-local-development-and-manual-deployment)
to deploy the custom package and apply cloud settings and vault permissions separately.
- Retain the [Easy Auth trust checks](ONBOARDING.md#manual-deployment-trust-checklist), then
+ Keep the [Easy Auth trust checks](ONBOARDING.md#manual-deployment-trust-checklist), then
complete the validation below.
@@ -206,10 +197,12 @@ invalid or tampered payloads, evaluation without a send, and controlled live tes
Check handset receipt or voice audio separately from acceptance. For local tests, use test keys
and fake service responses on loopback only, without tunnels; Core Tools has no Easy Auth.
-The sample has **no early acknowledgement/background delivery, durable queue, automatic send
-retry, or deduplication**. Its outbound timeout is not a whole-request deadline. A timed-out send
-may already have been accepted, so retries or caller fallback can cause duplicates.
+The sample completes the request after provider acceptance. A live design can add early
+acknowledgement, background delivery, a durable queue, retries, or duplicate-send protection.
+Define the timing and owner for each feature. The outbound timeout covers the provider call,
+not the whole request. Check a timed-out send before retrying because the provider may already
+have accepted it.
Keep phone numbers, messages, passcodes, nonces, tokens, keys, JWE contents, and raw service
responses out of logs. Disable body tracing in the application, platform, and proxies.
-Review the remaining [production limitations](CONTRACT.md#production-limitations) before activation.
+Review the [production-readiness considerations](CONTRACT.md#production-limitations) before activation.
diff --git a/docs/CONTRACT.md b/docs/CONTRACT.md
index d9b5f36..4e0979d 100644
--- a/docs/CONTRACT.md
+++ b/docs/CONTRACT.md
@@ -1,31 +1,31 @@
-# External Phone Provider Function: Language-Agnostic Contract
+# HTTP contract
-This defines the shared contract for [JavaScript](../javascript/), [Python](../python/) and
-[.NET](../dotnet/). See [production limitations](#production-limitations) before production use.
+
-> **Naming.** EPP means **External Phone Provider**. App settings use the `EPP_` prefix; the
-> request and delivery models are `Envelope`, `DeliveryContext` and provider-neutral OTP delivery.
-> Documentation names do not change the external JSON fields or `microsoft.mfa.otpDeliver.v1` version.
+[Documentation](README.md) | [Implementation walkthrough](BRING-YOUR-OWN-PROVIDER.md) | [Settings and packages](../TECHNICAL.md)
-The design selects one provider per deployment. The HTTP Function owns the request lifecycle, while
-API-specific credentials, paths, headers, payloads, response rules, outcomes and safe failure
-classifications remain inside each provider implementation.
+Shared contract for [JavaScript](../javascript/), [Python](../python/), and [.NET](../dotnet/).
-An [optional manual Front Door topology](FRONTDOOR.md) can route this same API to regional origins.
-It does not change caller authentication, JWE validation, nonce semantics, or provider selection,
-and it does not add automatic provider failover or delivery deduplication. Any readiness endpoint
-is separate from this contract and must not deliver OTPs.
+Go to [request/response](#1-http-api), [HTTP outcomes](#2-outcome--http-status-mapping),
+[provider contract](#3-provider-contract), [settings](#4-configuration-app-settings--env),
+[caching](#credential-caching-and-refresh), [required behaviors](#5-required-behaviors),
+[logs](#application-logs), or [tests](#6-lightweight-tests).
+
+> **Naming:** EPP means **External Phone Provider**. App settings keep the `EPP_` prefix;
+> documentation names do not change external fields or `microsoft.mfa.otpDeliver.v1`.
+
+Each deployment selects one provider. Provider-specific API behavior stays inside its implementation.
+
+The [Front Door design](FRONTDOOR.md) routes this API to regional origins. Its readiness endpoint
+is separate and never delivers OTPs.
---
## 1. HTTP API
-**Endpoint:** `POST /api/SendOtp` (Functions HTTP trigger, `authLevel: anonymous`). **Easy Auth is the
-only caller-authentication boundary and runs before the handler**; the application has no token
-validator or function-key gate. This is the interface **Microsoft Entra ID** calls.
-Entra ID sends a signed bearer JWT; Easy Auth validates it, not the handler.
-PII (phone number + the rendered message, which contains the passcode) is **encrypted** inside a JWE;
-the cleartext envelope carries routing/scheduling only.
+**Endpoint:** `POST /api/SendOtp` (`authLevel: anonymous`). **Easy Auth is the caller-authentication
+boundary before the handler**. The phone number and message are encrypted inside a JWE; the
+cleartext envelope carries routing data.
### Request headers
@@ -36,11 +36,11 @@ the cleartext envelope carries routing/scheduling only.
| `x-ms-correlation-id` | tracing only; fallback for envelope `correlationId`, not authentication |
| `x-ms-client-request-id` | per-attempt tracing id (used as `messageId`), not authentication |
-Forwarded headers, including `x-ms-client-principal`, do not establish trust by themselves and cannot
-replace the required Easy Auth gate. The handler does not use them to authenticate callers or forward
-the incoming `Authorization` header to the provider. Configure Easy Auth as described in section 5.
+Forwarded headers such as `x-ms-client-principal` provide context, not authentication. The handler
+never forwards `Authorization` to the provider.
-### EPP request body: `Envelope` (cleartext envelope)
+
+### EPP request body
| Field | Required | Notes |
|-------|----------|-------|
@@ -78,8 +78,8 @@ advisory key-ID check must not replace the original protected-header bytes used
`EPP_DECRYPTION_KEY_PEM` accepts a private-key PEM alone or a PEM certificate bundle containing the
private key, in plain text or base64 form. Guided setup issues a certificate inside Key Vault or
-reuses an explicitly selected certificate version, pins its PEM backing-secret reference, and
-keeps exactly one approved encryption credential on the application. Certificate synchronization
+reuses a selected certificate version. It pins the PEM backing-secret reference and keeps exactly
+one approved encryption credential on the application. Certificate synchronization
is manual; Key Vault renewal does not automatically update Entra or select another decryption key. See the
[certificate lifecycle](../setup/docs/README.md#encryption-certificate-lifecycle).
@@ -286,7 +286,7 @@ the transport result and returns a normalized provider result. Raw API-specific
by the Function and never enters public responses or logs.
This model is internal: do not serialize it into the endpoint response or logs. The
-[request logger](#application-logs) selects only the provider HTTP status, a recognized normalized
+[request logger](LOGGING.md) selects only the provider HTTP status, a recognized normalized
status, the mapped outcome and a bounded raw provider message ID; descriptions and raw
metadata remain private. Public HTTP responses still expose only the existing
nonce/correlation/status or sanitized error contract.
@@ -428,10 +428,10 @@ stop polling/prevent late publication at shutdown. .NET has no poller to stop: f
fills are not retained and later misses attempt retrieval. It does not proactively refresh a
still-valid entry.
-JavaScript bounds shared acquisition to 2.5 seconds and propagates cancellation through the SDK
-transport. Python bounds waits and SDK connect/read inactivity to 2.5 seconds but cannot forcibly
-cancel synchronous I/O. .NET uses a 2.5-second fetch budget linked to the fetch caller's cancellation
-token. None is a whole-invocation deadline.
+JavaScript limits shared acquisition to 2.5 seconds and passes cancellation through the SDK
+transport. Python limits waits and SDK connect/read inactivity to 2.5 seconds. Synchronous I/O
+may continue until the underlying call returns. .NET uses a 2.5-second fetch budget linked to
+the caller's cancellation token. None is a whole-invocation deadline.
All runtimes fetch a complete API-key/customer-ID bundle before use and use managed identity
for vault access. With caching enabled, Soprano reuses managed-identity and client-assertion SDK credentials
@@ -448,7 +448,7 @@ Do not log SDK exception details or tokens. No cache retries/deduplicates provid
## 5. Required behaviors
-- **Fail-closed**: only `Continue` → `200 accepted`; unknown status → `Fail`.
+- **Safe outcome mapping (fail-closed)**: only `Continue` → `200 accepted`; unknown status → `Fail`.
- **Managed identity**: Key Vault access via managed identity only (user-assigned if `AZURE_CLIENT_ID`
set, else system-assigned). No static credentials.
- **Privacy**: never log phone numbers, passcodes, nonce values, bearer tokens, API keys, JWE headers/payloads,
@@ -467,154 +467,24 @@ Do not log SDK exception details or tokens. No cache retries/deduplicates provid
the authorized Microsoft caller application ID. No excluded path may bypass authentication for SendOtp.
The trigger remains `authLevel: anonymous`; there is no application token validation or function-key
fallback. The platform rejects unauthenticated requests with `401` and denies callers outside the
- allowlist before the handler. **Never expose the endpoint to the public internet with Easy Auth off
- or bypassed.** Core Tools supplies no Easy Auth: local execution must bind only to loopback, with
- no tunnels or public forwarding. Neither request data, JWE decryption, a fixed nonce nor forwarded
- principal headers authenticate the Entra ID caller.
+ allowlist before the handler. **Keep Easy Auth enabled for every public SendOtp route.**
+ Core Tools supplies no Easy Auth, so local execution stays on loopback without tunnels or public
+ forwarding. Request data, JWE decryption, a fixed nonce, and forwarded principal headers serve
+ other parts of the contract; Easy Auth authenticates the Entra ID caller.
- **Timeout boundaries**: platform authentication and Key Vault retrieval happen outside the outbound HTTP
timer. Python uses connect/read inactivity timeouts, not a hard elapsed-time deadline. The cap
- therefore does not guarantee a 3.2-second end-to-end response, especially on cold starts.
+ does not guarantee a 3.2-second end-to-end response, especially on cold starts.
A timed-out POST may already have been accepted; avoid blind retries that duplicate messages.
### Application logs
-JavaScript emits JSON service records with an immutable request context. Python uses the standard
-`logging` pipeline: `otp_log.py` defines fixed event IDs, names, levels and fields, while an immutable
-`LoggerAdapter` context supplies the Function and Microsoft trace identifiers. The configured
-logging provider owns Python output formatting and export. Neither runtime manually builds a mutable
-comprehensive request summary; each invocation ends with a fixed `request_completed` event.
-
-Likewise, .NET uses the standard `ILogger` pipeline instead of manually serializing JSON. `OtpLog`
-defines source-generated events with stable IDs and names, while `ILogger.BeginScope` supplies
-`FunctionName`, `FunctionRequestId`, `FunctionInvocationId`, `MsClientRequestId`,
-`MsCorrelationId` and `MsCorrelationIdSource`. The configured logging provider owns output
-formatting and export. .NET emits `request_completed` as an ordinary typed event rather than a
-mutable comprehensive summary. JavaScript follows the same fixed-event model while retaining its
-existing JSON field names.
-
-A successful live request in each runtime emits:
-
-`request_received`, `payload_validated` (`envelope_validated` in JavaScript),
-`delivery_context_decrypted`, `provider_selected`,
-`provider_credential_resolution_started`, `provider_credential_resolved`,
-`provider_request_build_started`, `provider_request_built`, `provider_request_started`,
-`provider_response_received`, `provider_response_processed`, `response_prepared`,
-`request_completed`.
-
-Failures emit `request_failed` with fixed `FailureStage`, `FailureReason` and `HttpStatus` values.
-Provider results retain the coarse endpoint `Outcome` plus a safe diagnostic classification:
-`provider_http_error`, `provider_rejected`, `unrecognized_provider_status`,
-`missing_provider_message_id` or `invalid_provider_json`. The structured
-`provider_response_processed` event includes this classification together with the upstream HTTP
-status and normalized provider status; raw provider descriptions and bodies remain excluded.
-Credential refresh failures use `credential_refresh_failed`; unexpected unclassified failures use
-`unexpected_error`. Typed event arguments are explicitly allowlisted and never include request
-bodies, decrypted delivery fields, credentials, provider response bodies, query strings or private
-exception messages. Endpoint values contain only scheme, host/port and path. Evaluation omits all
-provider events and emits `evaluation_completed`.
-
-A successful live request emits these separate service events:
-
-| Service event | Safe information recorded |
-|---|---|
-| `request_received` | Function invocation and available raw Microsoft trace IDs under their `x-ms-*` names; no raw body or arbitrary headers. |
-| `envelope_validated` / `payload_validated` | Allowlisted body metadata: validated payload type, normalized `channel`, `evaluation` and optional `ttlSeconds`. |
-| `delivery_context_decrypted` | Decryption completed; no plaintext fields, JWE or key ID. |
-| `provider_selected` | Registered provider and its authentication mode. |
-| `provider_credential_resolution_started` | OAuth client-assertion or Key Vault credential source, with explicitly named raw OAuth application/identity/tenant IDs. |
-| `provider_credential_resolved` | Credential resolution and usability checks completed, with elapsed time; no secret, token, assertion or claims. |
-| `provider_request_build_started` | The provider is preparing the outbound request. |
-| `provider_request_built` | Allowlisted HTTP method, final endpoint scheme/host/port/API path, HTTPS and disabled redirects; no query string, authorization headers or body. |
-| `provider_request_started` | The outbound send is beginning, with method, sanitized endpoint and timeout. |
-| `provider_response_received` | Actual upstream HTTP status; emitted before response-body reading completes. |
-| `provider_response_processed` | Mapped provider status/outcome, fixed failure classification and duration. Raw provider descriptions and bodies are excluded. |
-| `response_prepared` | Response status and booleans indicating nonce/correlation inclusion, not their values or the response body. |
-
-Body metadata is built from validated fields, **not** from a body dump with a few sensitive
-properties removed. Unknown request properties, `tenantId`, locale/risk data and all decrypted
-fields remain excluded. An invalid envelope never contributes unvalidated type or TTL values.
-
-`providerCredentialSource` is `managed_identity_client_assertion` for the app-token path and
-`key_vault` for API-key providers. Resolution may use cached credentials: these events do not claim
-a Key Vault network fetch or a fresh identity/token exchange occurred. `providerCredentialElapsedMs`
-covers resolution plus the existing credential checks, not a separately enforced deadline. SDK
-diagnostics remain suppressed as before; logging adds no token acquisition, secret reads or retries.
-
-`provider_request_built` describes the provider request, not creation of a new physical HTTP connection
-or a new HTTP client in every runtime. `providerEndpoint` uses the **final** provider URL, which can
-differ from the configured base URL, but records only scheme + host/port + API path. For example,
-`https://provider.example/epp/voice?key=secret` is logged as `https://provider.example/epp/voice`.
-Userinfo, the entire query string and fragments are excluded. Use provider-approved paths that do
-not embed secrets or personal data; path segments are not redacted. This does not alter the outbound
-URL or its required query parameters. Standard HTTP verbs are logged in uppercase; custom or invalid
-values are represented as `other` without changing the request sent.
-
-`response_prepared` is emitted on success **and failure** immediately before returning the handler
-response. It does not claim the host has serialized/transmitted that response or Microsoft received
-it; consult platform request telemetry for transport completion. Evaluation emits
-`evaluation_completed` instead of provider events, then `response_prepared` and `request_completed`,
-without resolving provider configuration, credentials or HTTP. Failures emit the fixed
-`request_failed` event with a safe stage, reason and HTTP status.
-A parsed provider rejection uses `provider_response_processed` with its non-success outcome and
-fixed failure reason.
-
-In JavaScript and Python, every service event carries the Function request/invocation IDs, the Microsoft trace IDs available
-at that point. JavaScript also carries known channel/evaluation/provider context; Python supplies
-selected context through its logging adapter and event arguments. The initial event can
-only know header trace IDs; a valid envelope can subsequently supply the selected correlation.
-The generated Function request ID joins these events even when Microsoft IDs are absent or change
-from header to envelope. The identifiers are intentionally not named simply `requestId` or
-`correlationId` in logs:
-
-| Log field | Source and meaning |
-|---|---|
-| `functionName` | Runtime function name: `SendOtp` in JavaScript/.NET, `send_otp` in Python. The HTTP route remains `/api/SendOtp` in every runtime. |
-| `functionRequestId` | Generated by this handler. Matches `requestId` in failure responses; it is not Microsoft's request ID. |
-| `functionInvocationId` | Azure Functions host invocation ID. Null for direct handler calls without a host context. |
-| `x-ms-client-request-id` | Raw Microsoft per-attempt ID from the header of the same name, not the generated fallback used by dispatch. |
-| `x-ms-correlation-id` | Raw selected Microsoft correlation: envelope `correlationId` first, then the header of the same name. The existing precedence is unchanged, and this never contains a generated Function ID. |
-| `msCorrelationIdSource` | `envelope`, `header` or `none`; disambiguates the selected value when the envelope and header differ. |
-| `providerMessageId` | Raw provider-normalized message/reference ID returned by the provider, including Telesign `reference_id`, for support lookup. Not filled from delivery or Function IDs, although a provider may echo an ID it received. |
-| `providerTenantId` | Raw configured `EPP_PROVIDER_TENANT_ID` for OAuth, not incoming envelope `tenantId`. |
-| `functionOutboundClientId` | Raw application's `EPP_OUTBOUND_CLIENT_ID` used for provider access, not a Microsoft request ID. Guided setup reuses the endpoint app client ID here; its outbound purpose is distinct from the inbound audience setting. |
-| `functionOutboundManagedIdentityClientId` | Raw configured `EPP_OUTBOUND_MI_CLIENT_ID` used to obtain the app assertion, not the Key Vault identity or the identity's principal/Object ID. |
-| `providerEndpoint` | Validated final outbound URL without userinfo, query string or fragment; scheme, host/port and API path remain visible. |
-
-The six external/configuration ID fields preserve their raw values and case for cross-system support
-lookup; they are not hashed or truncated. To avoid dumping arbitrary text, only nonblank strings
-of 1-128 ASCII characters are recorded: the first character must be a letter or digit, followed by
-letters, digits, `.`, `_`, `:`, or `-`. This includes GUIDs, hex IDs and ordinary opaque references.
-Missing/blank values are null; other invalid values are null and their field names appear in
-`omittedIdFields`, never the rejected values. These guards affect logs only, not wire IDs or outcomes.
-Application/client/tenant IDs are identifiers, not client secrets or bearer tokens.
-These are tracing fields, not authentication assertions. In particular, an incoming `tenantId`
-does not become a trusted tenant identity in logs. The existing wire correlation precedence,
-provider request IDs and public responses are unchanged.
-
-The fixed `request_completed` event contains only the final HTTP status, result and elapsed time.
-Stage-specific fields remain on the event where they become known instead of being accumulated into
-a mutable summary. `providerHttpStatus` is recorded when response headers arrive, so a response-body
-timeout can legitimately produce `provider_response_received` with upstream `200` followed by
-`request_failed` with Function status `504`, without a mapped provider status or success
-acknowledgement. Unknown provider status text and malformed JSON are never logged; malformed JSON
-emits only `provider_response_invalid_json` before the provider outcome rules run.
-
-Normal events and completion events use Information; invalid requests, non-success 4xx outcomes and
-advisory warnings use Warning; 5xx failures and timeouts use Error. Keep application Information logs
-enabled when investigating. This contract describes **emission**, not guaranteed collection:
-host/telemetry filters and sampling can drop trace records. Application events are logs, not
-the host's Request telemetry type, so excluding `Request` from sampling does not by itself retain
-every event. Configure collection and retention deliberately without enabling SDK/body tracing.
-Easy Auth rejections occur before the handler; inspect available platform/authentication diagnostics
-and caller observations, not just application events.
-
-The camelCase/`x-ms-*` fields above describe the JavaScript schema and are **not a portable
-custom-properties contract**. Python uses `event_name` with logging extras such as `httpStatus`;
-.NET uses typed `ILogger` fields such as `HttpStatus` and scopes such as `FunctionRequestId`.
-The simpler Python/.NET provider-result events do not expose every JavaScript field (for example,
-do not assume a provider reference ID is collected). Exporters can omit extras/scopes.
-Use [runtime-specific queries](MONITORING.md#runtime-specific-log-discovery) and
-[collection guidance](APPLICATION-INSIGHTS.md) to verify what actually arrives.
+See the [application log reference](LOGGING.md) for event order, allowlisted fields, identifier
+provenance, levels, and runtime formats. Each invocation emits fixed events ending in
+`request_completed`, not a mutable request summary. Evaluation emits no provider events.
+Keep bodies, nonces, credentials, and raw exceptions out of every pipeline.
+
+Emission is not guaranteed collection. Use [runtime-specific queries](MONITORING.md#runtime-specific-log-discovery)
+and [collection diagnostics](APPLICATION-INSIGHTS.md) before relying on alerts.
---
@@ -640,28 +510,32 @@ wrong issuer or audience, unauthorized caller, HTTPS enforcement and SendOtp rou
An authorized evaluation must succeed without provider I/O. These checks do not replace provider
integration or handset-delivery checks. See [deployment verification](ONBOARDING.md#4-package-deploy-and-verify).
-## Production limitations
-
-This is a sample, not production certification. Successful provider acceptance and nonce checks do
-not prove handset delivery or support for every provider feature.
-
-- The Function imports one private PEM through a Key Vault secret reference and decrypts in-process;
- vault-resident cryptographic operations and overlapping key rotation are not implemented.
-- The sample waits for provider acceptance before replying. It has no durable queue, early
- acknowledgement/background delivery handoff, or automatic send retries. Confirm the current
- product-approved timing/activation requirements with Microsoft; deployment alone does not
- establish that the sample meets them.
-- The outbound timeout is not an end-to-end deadline. Cold starts, platform authentication and Key
- Vault access can exceed the caller's budget; Python uses connect/read inactivity timeouts.
-- Credential caches are process-local and proactive preparation is best-effort, not an Azure
- traffic-readiness guarantee. Provider delivery still waits for acceptance; background refresh
- is not background OTP delivery, a queue, or protection against duplicate sends.
-- Voice is provider-specific: Soprano extracts the first six consecutive digits; Telesign paces
- standalone six-digit numeric runs and repeats the full message. Test supported locales,
- passcode format, and actual audio; these transforms do not guarantee spoken clarity.
-- Full body-size/content-type and E.164 validation, subscription provisioning, certification,
- least-cost routing and voice-callback workflows are outside this sample. Native fallback belongs
- to the caller, not this Function.
+
+
+## Production readiness
+
+Use this checklist to plan capabilities required beyond the sample.
+Provider acceptance and nonce checks confirm important stages of the request, while handset
+delivery and provider-specific features need their own validation.
+
+- **Encryption and rotation:** the Function imports one private PEM through a Key Vault secret
+ reference and decrypts in-process. A production design can add vault-resident cryptographic
+ operations and overlapping key rotation when required.
+- **Delivery workflow:** the sample waits for provider acceptance before replying. Add a durable
+ queue, early acknowledgement/background handoff, or automated retry strategy only when the
+ approved product design defines their timing and duplicate-send behavior.
+- **End-to-end timing:** size the caller budget for cold starts, platform authentication, Key Vault
+ access, and the provider request. The outbound timeout covers the provider call rather than the
+ full invocation; Python uses connect/read inactivity timeouts.
+- **Credential readiness:** caches are process-local and proactive preparation is best-effort.
+ Provider delivery still waits for acceptance; background credential refresh is separate from
+ OTP delivery, queuing, and duplicate-send protection.
+- **Voice experience:** Soprano extracts the first six consecutive digits; Telesign paces
+ standalone six-digit numeric runs and repeats the full message. Validate supported locales,
+ passcode formats, and actual audio for the intended experience.
+- **Additional production capabilities:** add the body-size/content-type and E.164 validation,
+ subscription setup, certification, least-cost routing, or voice-callback workflows required
+ by the design. Native caller fallback remains with the caller rather than this Function.
See [setup compatibility](ONBOARDING.md#setup-script-compatibility) for credential provisioning,
endpoint format and unsupported setup options.
diff --git a/docs/COST-CALCULATOR.md b/docs/COST-CALCULATOR.md
index f2dbe54..a1e38f7 100644
--- a/docs/COST-CALCULATOR.md
+++ b/docs/COST-CALCULATOR.md
@@ -1,24 +1,17 @@
# Estimate SMS provider costs
-Use this guide to create a planning estimate for SMS traffic before onboarding an
-External Phone Provider (EPP). The report finds SMS and voice authentication steps in
-recent sign-in activity and groups them by the phone number's international calling
-code, such as `+1` or `+91`. Apply your provider's destination rates to the report to
-estimate a budget.
-
-**This is an estimate, not a usage or billing report.** A sign-in does not prove that
-the provider accepted or delivered an SMS or voice call. Authentication details can
-contain multiple SMS or voice steps for one sign-in, so this guide counts each matching
-step as one estimated request. Actual provider billing can differ because of retries,
-fallback, failures, message segmentation, and provider-specific charging rules. Confirm
-the assumptions, supported destinations, taxes, fees, and final pricing with your provider.
+Estimate SMS and voice traffic before onboarding an External Phone Provider (EPP). The report
+groups recent authentication steps by calling code, such as `+1` or `+91`. Apply provider rates
+to estimate a budget.
+
+**This is not a billing report.** It counts each matching authentication step as one estimated
+request. Confirm retries, fallback, segmentation, destinations, taxes, fees, and pricing with
+the provider.
The `authenticationDetails` data used by this guide is currently available through the
[Microsoft Graph beta API](https://learn.microsoft.com/en-us/graph/api/resources/authenticationdetail?view=graph-rest-beta).
-Beta APIs are subject to change and are not supported for production applications. Review
-and test this reporting script before each planning exercise. Because sign-in details mask
-phone numbers, the script matches their visible trailing digits to the user's registered
-phone authentication methods and derives the calling code from the matched registration.
+Review the beta API and script before each planning exercise. The script matches masked trailing
+digits to registered phone methods to derive the calling code.
## Prerequisites
@@ -28,28 +21,22 @@ account must be allowed to consent to or use these Microsoft Graph delegated per
- `AuditLog.Read.All`
- `UserAuthenticationMethod.Read.All`
-Reading other users' phone methods also requires a
+Reading other users' phone methods requires a
[supported Entra role](https://learn.microsoft.com/en-us/graph/api/authentication-list-phonemethods?view=graph-rest-1.0#permissions),
-not just consent to those scopes. Authentication Administrator receives masked phone numbers;
-if the calling code is unavailable, the script reports `Unknown`. Confirm approved read access
-with your identity administrator rather than granting broader roles just to fill the report.
+Authentication Administrator receives masked phone numbers; unresolved calling codes appear as `Unknown`.
-Access to sign-in logs also depends on your Microsoft Entra licensing, directory role,
-and log-retention period. Review the requested permissions before consenting. The script
-processes sign-in authentication details that can include phone numbers. It exports only
-aggregated calling codes, methods, and counts, not full phone numbers. Store, share, and
-delete the output according to your organization's privacy and retention requirements.
+Access also depends on Microsoft Entra licensing, roles, and retention. The script exports
+aggregated calling codes, methods, and counts, not full phone numbers. Protect the output.
Create `C:\temp` before running the script, or change the `Export-Csv` path to an
approved folder.
## Generate the activity report
-The following sample installs the required Microsoft Graph modules for the current user,
-connects to Microsoft Graph, identifies SMS and voice authentication steps, and matches
-each masked phone detail to a registered phone method. It caches phone-method lookups so
-each user is queried only once. Replace `$start` and `$end` with the period you want to
-analyze.
+The sample installs the required Microsoft Graph modules and connects to Microsoft Graph.
+It finds SMS and voice authentication steps, matches each masked phone detail to a registered
+phone method, and caches each user's phone-method lookup. Set `$start` and `$end` to the
+required reporting period.
```powershell
# 1. Install required modules if they are missing
@@ -176,11 +163,10 @@ from that detail and matches the visible suffix (`90`) against the user's regist
methods. It then extracts the calling code from the uniquely matched registered number,
whose Graph format includes a space after the code, such as `+91 1234567890`.
-For SMS, only `mobile` and `alternateMobile` registrations are considered; voice can use
-any registered phone type. A row is grouped as `Unknown` when the masked detail exposes
-fewer than two digits, no registered number matches, more than one registered number has
-the same visible suffix, or the matched number has an unexpected format. This avoids
-guessing a destination. Review the `Unknown` total before relying on the estimate.
+For SMS, the script uses `mobile` and `alternateMobile` registrations. Voice can use any
+registered phone type. The report uses `Unknown` when the visible detail is too short,
+does not match one registered number, or has an unexpected format. This avoids guessing
+a destination. Review the `Unknown` total before relying on the estimate.
The script does not use sign-in geography. `-All` requests all available pages, but large
tenants should still account for Microsoft Graph throttling and execution time. The script
@@ -216,9 +202,10 @@ Apply the provider's destination-specific rates to those estimated requests. Use
rows when rates differ by destination, sender type, route, or message category. Include a
contingency for growth, retries, fallback behavior, and seasonal peaks.
-## Confirm the final estimate with your provider
+
+## Confirm provider rates
-Work with your provider to validate:
+Confirm the following with the provider:
- How each international calling code maps to the provider's destination pricing.
- Country and carrier coverage, sender registration, and route-specific rates.
diff --git a/docs/FRONTDOOR.md b/docs/FRONTDOOR.md
index f6020ab..c9cd1aa 100644
--- a/docs/FRONTDOOR.md
+++ b/docs/FRONTDOOR.md
@@ -1,59 +1,47 @@
-# Optional multi-region onboarding with Azure Front Door
+# Azure Front Door multi-region deployment
-Use Azure Front Door when you want **one public EPP URL backed by Function Apps in multiple regions**.
-You configure this option manually. For a single region, start with the
-[main onboarding guide](../README.md).
+
+
-**We don't provide a Front Door deployment script.** The guided setup creates one regional endpoint,
-not the multi-region setup described here. This guide covers what you need to configure and test.
-Failover can still interrupt requests.
+[Documentation](README.md) | [Single-region setup](GETTING-STARTED.md) | [Routing troubleshooting](../setup/docs/Troubleshooting.md#optional-front-door-deployments)
+
+Azure Front Door provides **one public EPP URL across regional Function Apps**. Start with a
+validated [single-region deployment](GETTING-STARTED.md), then add origins, readiness, routing,
+validation, failover drills, and rollback.
+
+Follow [scope](#scope-and-observed-behavior), [architecture](#architecture),
+[plan/capacity](#1-plan-the-regional-deployment), [origins/keys](#2-prepare-equivalent-independently-provisioned-origins),
+[readiness](#3-provide-a-non-delivering-readiness-endpoint), [routing](#4-configure-front-door-manually),
+[validation](#5-validate-before-manually-activating-policy), and [fire-drill](#6-regional-failover-fire-drill).
+Use [historical results](#observed-failover-results-and-limitations) as test context.
## Scope and observed behavior
-We tested an isolated JavaScript deployment with Front Door Standard, two and three regional
-origins, a shared encryption certificate, and a separate readiness handler that sends no messages.
-The tests covered encrypted evaluation requests, rejection of invalid callers, direct-origin
-access restrictions, and stopping and restarting individual Functions.
+Testing used JavaScript origins, Front Door Standard, two or three regions, a shared key, and a
+non-delivering readiness handler.
-**The readiness handler used in testing is not part of the released sample.** You must implement
-and validate the [readiness contract below](#3-provide-a-non-delivering-readiness-endpoint) before
-enabling multi-origin health probes. There is no app setting that adds this endpoint to a release ZIP.
-You can adapt the design for other runtimes, but we haven't tested it with .NET or Python.
+Add the [readiness contract](#3-provide-a-non-delivering-readiness-endpoint) to each regional
+package before enabling probes. Released packages remain single-region.
-We **did not test** live SMS/voice delivery, calls and retries from the real Entra service, a full
-Azure regional outage, custom domains, WAF, Private Link, or production load. Test the features your
-deployment needs before activating policy.
+Build acceptance tests around the selected runtime and features, including approved live checks,
+Microsoft caller behavior, dependency loss, networking, and realistic load.
### Regional resiliency responsibility
-For the Microsoft-configured baseline, Microsoft is responsible for defining, configuring,
-documenting, and validating the standard regional-failover pattern and its exercise procedure.
-Customers using that baseline should not have to design regional disaster recovery (DR) themselves.
-Customers operate their deployed baseline: assign
-owners, maintain regional capacity, credentials and keys, monitor it, and run approved drills.
-Customers who replace the topology own the alternate regional-failover design and its validation.
-Customer operation of the standard baseline does not transfer responsibility for designing that
-baseline to the customer.
+Customers own capacity, credentials, keys, monitoring, and approved drills. Validate any
+alternate topology separately.
-The two-region pattern below makes the intended baseline concrete, but today's onboarding remains
-single-region and Front Door setup remains manual. The readiness-handler, regional-dependency,
-capacity, real-caller, and outage-validation gaps described here remain to be addressed before
-relying on this pattern for regional recovery. Recovery-time and recovery-point objectives
-(RTO/RPO), tolerable request failures, latency limits, and required evidence must be defined and
-approved by the deployment owners; the observed results below do not establish those targets.
+Set RTO/RPO, failure, latency, capacity, and evidence targets for the regional design.
-## Architecture: one URL, multiple origins
+
+## Architecture

-The diagram shows the design, not the exact test deployment. Its region names are examples.
-Custom domains and Premium/WAF are optional and weren't part of the Standard-tier test.
-Give each Function App, storage account, and Key Vault a globally unique name. Each region also has
-its own identity IDs and vault references. Keep the **provider and authentication configuration
-consistent and use the same RSA key**, rather than copying every setting literally.
-The SMS/voice arrows show live delivery. Evaluation returns the nonce without contacting the provider.
-The diagram's provider-secret arrows apply to API-key integrations; OAuth uses each region's
-outbound managed identity. Match the key material across regions, not the vault URLs or version IDs.
+Region names are examples. Give each Function App, storage account, and Key Vault a unique name.
+Keep **provider and authentication configuration consistent and use the same RSA key**.
+Evaluation returns the nonce without contacting the provider. API-key integrations use secrets;
+OAuth uses each region's outbound identity. Match key material, not vault URLs or version IDs.
| Component | Purpose |
|---|---|
@@ -62,32 +50,23 @@ outbound managed identity. Match the key material across regions, not the vault
| Origin group | Contain the regional Function hostnames and their health, priority, weight, and latency settings. |
| Regional origin | Run the same selected implementation and provider integration, with local storage, Key Vault, identity, and telemetry. |
-The caller uses `https:///api/SendOtp`, not a list of regional URLs.
-Front Door chooses an origin based on health, priority, latency, and weight. Equal priorities
-allow both origins to serve traffic. Equal weights do **not** guarantee a 50/50 split. Adding a third region
-normally adds an **origin**, not another customer-facing endpoint.
+The caller uses `https:///api/SendOtp`. Front Door selects an origin by health,
+priority, latency, and weight. Equal weights do not guarantee a 50/50 split.
### Two-region baseline to provision and validate
-Use two independently provisioned regional stacks, A and B, at equal Front Door priority for
-active-active service. Each has its own Function App and hosting capacity, private runtime/package
-storage, Key Vault containing the shared decryption key and applicable provider credentials,
-managed identities with local scoped access, and regional telemetry. Configure provider federation
-for each regional outbound identity where required. Neither stack may require the other region's
-storage, vault, or identity to serve or recover; verify startup, key resolution, and credential
-refresh as well as already-warm requests.
-
-Keep one shared Front Door public SendOtp URL and endpoint application registration, with identical
-issuer, audience, authorized-caller policy, selected provider route, and encryption public-key
-identity across both stacks. Replicate and renew the matching private key securely in each regional
-vault using the [key coordination procedure](#coordinate-the-encryption-key). Failover must not
-require changing the caller URL, trust configuration, or registered encryption certificate.
-Front Door, Entra authentication, and the selected provider remain shared dependencies; regional
-duplication does not add provider failover or remove those dependencies.
+Use two regional stacks at equal priority. Each has its own Function App, capacity, storage,
+Key Vault, identities, telemetry, matching decryption key, and provider credentials. Verify each
+can operate without the other region.
+
+Share the public URL, endpoint app, issuer, audience, caller policy, provider route, and public-key
+identity. Replicate the private key using [key coordination](#coordinate-the-encryption-key).
+Front Door, Entra authentication, and the provider remain shared dependencies.
## 1. Plan the regional deployment
-Start in a dedicated nonproduction tenant and subscription. Review the
+Build the regional design in an isolated nonproduction tenant and subscription, separate from
+live traffic. Review the
[setup prerequisites](../setup/docs/README.md#prerequisites-for-step-2) and
[regional quota guidance](../setup/docs/Troubleshooting.md#deployment-fails-with-subscriptionisoverquotaforsku).
@@ -95,7 +74,7 @@ Start in a dedicated nonproduction tenant and subscription. Review the
Security Store provider's requirements. A third region adds cost and capacity, but does not
necessarily shorten failure detection.
- Check availability of **every** required resource type, not just EP1 quota. Hosting availability
- doesn't mean Application Insights or other dependencies are available there.
+ does not mean Application Insights or other dependencies are available there.
- Budget for an EP1 plan and regional dependencies in each region, Front Door, and telemetry.
Size and validate each region for the survivor workload described below, not just its normal share.
- Choose one language and verify that each origin receives the same package. Do not mix
@@ -104,51 +83,49 @@ Start in a dedicated nonproduction tenant and subscription. Review the
caller application ID, certificate/public-key identity, selected provider route, and regional owners.
- Plan manual rollout, validation, certificate renewal, monitoring, and rollback before activation.
-**Do not simply rerun `Setup-Epp.ps1` in another region against the same application.** It is a
-single-region setup flow and does not coordinate Front Door, shared key material, regional
-registrations, or failover. Independent certificate issuance or a setup rerun can change app
-configuration or disable incoming access to an existing endpoint.
+Use the regional procedure in this guide rather than rerunning `Setup-Epp.ps1` against the same
+application. The setup script creates one region; the steps below coordinate Front Door, shared
+key material, regional registrations, and failover without replacing an existing endpoint's
+application configuration or certificate unexpectedly.
-### Capacity gate: each region must support the combined peak load
+
+### Regional capacity
-**EACH region must be provisioned and validated to serve the combined peak workload on its own.**
-For a balanced two-region active-active baseline, this means **at least twice its normal per-region
-traffic**, plus explicitly planned headroom for bursts, retries, probe traffic, and dependency
-overhead. Reserve that capacity in both A and B before relying on regional failover; spare capacity
-only in one direction is insufficient. Planning retry headroom does not authorize blind retries
-of live sends.
+**Size and validate each region to serve the full combined peak workload.** In a balanced
+two-region active-active design, each region needs capacity for **at least twice its normal
+traffic**. Add room for bursts, approved retries, probes, and dependencies. Reserve this
+capacity in both A and B before using regional failover. Retry capacity does not change the rule
+to check an uncertain live send before retrying.
-Use the expected peak offered workload, not a quiet-period average or the lower throughput left
-after requests time out. Equal weights do not guarantee equal routing. If normal traffic is
-unequal, each region still needs the entire combined peak workload plus headroom; the less-used
-region may need more than twice its usual traffic. An N-region alternative needs a documented
-failure budget and measured capacity for every permitted survivor set and routing distribution.
-Adding origins does not waive the double-load requirement for the balanced two-region baseline.
+Use the expected peak workload rather than a quiet-period average. Equal weights do not guarantee
+equal routing. If traffic is uneven, each region still needs the combined peak plus headroom, so
+the less-used region may need more than twice its usual traffic. For more than two regions,
+document the failure budget and measure capacity for every allowed survivor group and routing
+pattern.
Record the workload mix, offered and completed request rates, concurrency, payload sizes, test
duration, per-region latency percentiles, errors/timeouts, resource saturation, and dependency
throttling. Verify hosting/subscription quotas, available warm capacity and scale-out delay,
storage and vault limits, identity/token-service capacity, and provider account/rate limits.
-Autoscale maxima, provisioned instance counts, and approved quotas alone are **not capacity proof**.
-Pass only against agreed latency/error and sustained-load criteria with measured evidence in
-both directions. No measured production-capacity result is supplied by this guide.
+Use measured load results in both failover directions. Autoscale limits, instance counts, and
+approved quotas are inputs, while sustained request rate, latency, errors, and saturation show
+whether the design meets the agreed capacity target. This guide does not include a measured
+production result.
Encrypted evaluation requests bypass request-path provider credential resolution and outbound
provider calls. A double-load evaluation drill proves only the exercised ingress, authentication,
decryption, and routing path, **not complete live-send capacity**. Background credential refresh
can still run independently; its presence does not prove live-path credential capacity.
-The capacity gate separately requires production-representative evidence in both survivor
-regions using a **non-delivering, provider-approved sandbox/stub** that exercises the live request
-path, credential acquisition/refresh, and representative provider latency and quota/throttling
-behavior, or separately authorized delivery validation. Document how the sandbox/stub represents
-production and obtain provider capacity/quota evidence for what it cannot reproduce; an instant
-success stub or bypassed credential lookup is insufficient. Evaluate cold starts, cache expiry,
-and credential/key refresh so warm caches do not hide a dependency on the failed region. Keep
-this separate from the evaluation-only fire-drill; do not silently enable live sends in that drill
-or mark the full capacity gate passed using evaluation-only throughput.
+For full-path capacity evidence, test both survivor regions with a **non-delivering,
+provider-approved sandbox or stub**. Exercise the live request path, credential refresh, realistic
+provider delay, and quota or throttling behavior. If the test system cannot match part of
+production, record the difference and add provider capacity evidence. Include cold starts, cache
+expiry, and key refresh so warm caches do not hide a regional dependency. Keep this test separate
+from the evaluation-only fire drill, and use live sends only through a separately approved test.
-## 2. Prepare equivalent, independently provisioned origins
+
+## 2. Prepare regional origins
Create each regional Function App and its dependencies through your approved Azure process.
Keep public access disabled while setting up authentication, keys, identities, and packages.
@@ -175,7 +152,7 @@ Keep public access disabled while setting up authentication, keys, identities, a
### Coordinate the encryption key
-The caller must be able to encrypt once for any eligible origin. Every origin therefore needs the
+The caller must be able to encrypt once for any eligible origin. Every origin needs the
**same RSA private key corresponding to the registered public certificate**. Front Door's HTTPS
certificate is separate from this JWE encryption certificate.
@@ -187,14 +164,14 @@ certificate is separate from this JWE encryption certificate.
- Give each Function identity scoped access to its regional vault. Set `EPP_DECRYPTION_KEY_PEM`
to the regional certificate's **versioned PEM backing-secret reference**.
- Verify that the public keys match and each region can decrypt a request. Matching vault names
- or secret-version strings alone doesn't prove this.
-- Restored certificates are independent copies. They don't stay in sync automatically. Coordinate renewal
+ or secret-version strings alone does not prove this.
+- Restored certificates are independent copies. They do not stay in sync automatically. Coordinate renewal
and Entra updates across every origin using the [certificate lifecycle guidance](../setup/docs/README.md#encryption-certificate-lifecycle).
That section explains the single-key constraints; apply the multi-region updates through an
approved manual procedure, not independent setup reruns. The sample has one active decryption
key; it does not implement overlapping multi-key rotation.
- Keep plaintext keys out of source, logs, tickets, and pipeline artifacts. Protect and remove
- temporary encrypted backups under your approved handling policy.
+ temporary encrypted backups under the approved handling policy.
## 3. Provide a non-delivering readiness endpoint
@@ -212,16 +189,17 @@ Implement a separate route such as **`/api/health/ready`** with this contract:
Never exempt `/api/SendOtp`, `/api/*`, or the whole application.
- Keep the origin-level Front Door access restrictions described below on the readiness route too.
-Readiness proves neither provider availability nor handset delivery. Validate those separately.
-Do not point probes at a nonexistent handler, use a cached static `200`, or disable Easy Auth to
-make probes succeed. Front Door treats only a `200` probe response as healthy.
+Readiness confirms that the handler and decryption key are usable. Validate provider availability
+and handset delivery separately. Point probes to the implemented readiness handler, return a live
+result rather than a cached static `200`, and keep Easy Auth enabled for SendOtp. Front Door treats
+only a `200` probe response as healthy.
If every origin is unhealthy, Front Door can still route requests across them. Keep authentication
and application failure handling effective independently of probe status.
## 4. Configure Front Door manually
In the Azure portal, create a Front Door Standard/Premium profile, an endpoint, and an origin group.
-These are the **settings we tested**. Treat them as a starting point, not a performance guarantee:
+These are the **tested starting settings**. Validate them against the required performance target:
| Setting | Tested value |
|---|---|
@@ -242,14 +220,15 @@ These are the **settings we tested**. Treat them as a starting point, not a perf
| Caching | Disabled |
Create the route under **Front Door manager**, associate it with the endpoint's domain, and select
-the origin group. Inspect **Origin groups > your group > Origins** to see the regional backends.
-You should see one public endpoint with several origins behind it.
+the origin group. Inspect **Origin groups > selected group > Origins** to view the regional backends.
+The profile should show one public endpoint with several origins.
Use the same request body and caller authorization end to end. Do not add header-replacement,
redirect, cache, or retry rules without separately validating their effect on authentication and OTP
delivery. A cached response cannot confirm a new delivery request. Front Door does not cache POST.
-### Restrict each origin to this Front Door profile
+
+### Restrict origin access
Before allowing Front Door to reach the Functions, configure their access restrictions:
@@ -289,21 +268,14 @@ preserve unrelated properties, and read the policy back to confirm the change. T
### Test scenarios to plan and record
-Use this checklist to agree what you will test and what counts as a pass. **These are proposed
-tests, not a report that they have all been run.** The historical JavaScript evaluation, access
-rejection, and individual app-stop/restart tests are described in
-[scope and observed behavior](#scope-and-observed-behavior) and
-[observed failover results](#observed-failover-results-and-limitations). Those results do not prove
-double-load capacity, complete dependency isolation, or a full regional outage.
-
-Run only in an approved, isolated nonproduction environment, with synthetic data, bounded load,
-and the [fire-drill's approval, abort, and restoration safeguards](#6-regional-failover-fire-drill).
-Use an authorized test harness calling the Front Door endpoint, not an actual Entra ID sign-in flow.
-**Function evaluation suppresses sends only inside this endpoint.** Confirm Entra ID fallback
-and retry behavior with Microsoft rather than assuming a whole sign-in flow is non-delivering.
-Entra ID sign-in and delivery testing require separate safety approval and are not part of this checklist.
-Agree load, headroom, recovery time, error/latency limits, soak duration, and restoration deadlines
-before starting; do not substitute the historical measurements for your acceptance targets.
+**Proposed tests, not completed results.** Historical [app-stop trials](#observed-failover-results-and-limitations)
+do not prove double-load capacity, dependency isolation, or a regional outage.
+Approve synthetic data, bounded load, and the [drill's abort/restoration safeguards](#6-regional-failover-fire-drill)
+in isolated nonproduction. Agree load/headroom, recovery, errors/latency, soak, and restoration deadlines first.
+
+Use an authorized harness, **not a real Microsoft Entra ID sign-in flow**. Evaluation suppresses
+sends only inside this endpoint; caller fallback/retries can still deliver. Confirm caller behavior
+with Microsoft and obtain separate approval for sign-in or delivery tests.
| What to test | How and where to test safely | Expected outcome / acceptance | Evidence to keep |
|---|---|---|---|
@@ -323,86 +295,52 @@ but is not a substitute for the separate capacity and dependency tests.
## 6. Regional failover fire-drill
-This runbook is for an approved, dedicated nonproduction deployment. It exercises health-based
-rerouting; it is not permission to disrupt production or a claim of a full Azure regional-outage
-test. Define numeric pass/abort thresholds and durations before starting, rather than adopting the
-observed failover timings below as an SLA.
-
-1. **Assign ownership and approve the boundary.** Name a drill lead, change approver, regional
- resource owners, load/telemetry observer, and restoration owner with a backup. Record the exact
- tenant, subscription, Front Door profile, origin group, Function Apps, target region, change
- window, maximum isolation duration, and restoration actions. Confirm no production policy or
- real users depend on these resources. Approve only one isolated test origin at a time.
-2. **Prepare bounded, non-delivering traffic and independent recovery.** Use an authorized test
- caller to send synthetic encrypted `mode: 2` evaluation requests through the shared URL; never
- use real OTPs, real recipient data, or live-send mode. Check nonce matches locally, not in shared
- logs. Set hard rate, concurrency, request-timeout, total-duration, and cost limits. Place the
- controller and evidence capture outside the target region. Test restoration access and arrange
- an independent deadline-triggered restoration mechanism or backup operator; the load controller
- must not be the only way to end the outage.
-3. **Establish the baseline and acceptance gates.** Save the approved origin/routing, authentication,
- ingress, package, key-reference, and regional-dependency configuration. Complete the security and
- evaluation checks in section 5. Confirm both origins are healthy and actually serving, using
- origin-specific diagnostics rather than only the shared readiness URL. Agree the offered peak
- rate plus headroom, minimum survivor soak duration, detection/recovery target, allowed failures
- and timeouts, latency percentiles, resource/dependency limits, and failback observation window.
- Include abort thresholds for an unhealthy survivor, unexpected delivery, security drift, lost
- observability, or an approaching isolation deadline. Capture steady-state evidence from every
- region and the client before injecting a fault.
-4. **Isolate ONE approved origin; leave it enabled in Front Door.** After confirming it is serving
- traffic, apply the approved reversible fault, for example stopping that test Function App.
- Timestamp the fault request and platform confirmation separately. Keep the other origin healthy
- and unchanged. Do not disable the target in Front Door: manual disabling tests control-plane
- removal, not health-probe outage detection. Never disable authentication, exempt SendOtp from
- Easy Auth, or weaken ingress to make the drill pass. Front Door can route across all origins
- when all probes fail; probe state is not a security boundary.
-5. **Observe detection and sustained survivor load.** Maintain the bounded combined-peak evaluation
- load plus planned headroom through Front Door, without exceeding the approved test ceiling.
- Capture client attempts, completed requests, every failed response and transport error, latency,
- safe correlation IDs, and timestamps alongside Front Door probe/access diagnostics and every
- region's routing, Function, storage, vault, identity, and telemetry signals. Do not treat missing
- regional telemetry as zero errors or use delayed health-percentage graphs to time recovery.
- Verify the surviving region sustains the required offered rate and agreed criteria for the
- full soak period, not merely one successful request. Label this as evaluation-path capacity,
- not full live-send capacity; the separate provider/credential evidence required by the capacity
- gate remains necessary. Record failures during detection even if later requests succeed;
- retries must not hide first-attempt errors.
-6. **Restore unconditionally, including on abort.** Stop fault injection and restore the target
- at the deadline or any abort threshold, or when the soak completes. Stop synthetic load if
- unsafe. Put restoration in the controller's guaranteed cleanup path and retain the independent
- safeguard for controller interruption or loss. Restore the saved approved state even when
- assertions fail; escalate immediately to the restoration owner if restoration cannot be
- confirmed. Do not start another fault or declare success while an origin remains impaired.
-7. **Fail back conservatively, then reverse the direction.** Verify the restored origin's own
- dependencies, key/credential resolution, readiness, and successful encrypted evaluations.
- Expect Front Door to resume routing when its health criteria are met; watch both origins as
- traffic returns and avoid immediately changing weights or driving a new surge. Require stable
- health, latency, errors, and capacity through the agreed observation window. Confirm all
- resources and routing are restored before repeating the same exercise with the other region
- isolated. Both directions must meet the evaluation-drill criteria; the full survivor-capacity
- gate additionally requires the separate production-representative live-path evidence above.
-8. **Record the outcome and gaps.** Retain sanitized configuration/package identifiers, approvals,
- workload and limits, expected versus observed routing, fault/probe/client/recovery timestamps,
- rates, latency/errors, capacity/dependency evidence, aborts, and restoration confirmation.
- Record pass/fail against each agreed criterion, including unmet or unmeasured gates, with
- follow-up owners and repeat-test dates. Never attach keys, bearer tokens, request bodies, OTPs,
- or nonce values. Repeat after material topology, package, authentication, key, provider, or
- capacity changes and at the operator-approved cadence.
-
-### What this drill does not prove
-
-Stopping a Function App simulates one application origin becoming unavailable. It does **not**
-simulate the loss of an Azure region's storage, vault, identity access, network paths, control
-plane, or telemetry, nor prove the surviving stack can start or refresh credentials without them.
-The regional-resiliency validation plan must separately validate loss of regional dependencies and telemetry,
-using approved nonproduction fault scenarios and capturing evidence outside the affected region.
-Include dependency refresh/startup and restoration behavior; a warm app-stop success alone leaves
-these regional-outage gaps open. Record any scenario that cannot be safely exercised as an
-unvalidated limitation requiring deployment-owner review, not a passing regional-outage test.
-
-Evaluation also does not prove real Entra caller deadlines/retries, provider acceptance, or handset
-delivery. Validate those separately with the provider and onboarding owner. Neither this runbook
-nor Front Door makes retries automatically safe or guarantees zero downtime.
+Run this drill in an approved nonproduction environment built for failover testing. It exercises
+health-based rerouting without affecting production and covers the documented scenario rather than
+every possible regional outage. Define numeric pass/abort thresholds and durations before starting
+instead of treating the observed timings below as an SLA.
+
+1. **Approve the boundary.** Name the drill, change, telemetry, and restoration owners plus a
+ backup. Record the exact test resources, window, isolation deadline, and restoration steps.
+ Confirm no production policy or real users depend on them; isolate only one origin at a time.
+2. **Prepare safe traffic and recovery.** Use an authorized caller with bounded encrypted
+ `mode: 2` evaluation traffic—never live sends or real recipient data. Set rate, concurrency,
+ timeout, duration, and cost limits. Keep the controller/evidence outside the target region and
+ verify an independent restoration mechanism or backup operator.
+3. **Set the baseline and gates.** Save the approved routing, authentication, ingress, package,
+ key, and dependency state. Confirm both origins are healthy and serving. Agree survivor load,
+ soak time, recovery target, allowed failures/timeouts, latency/resource limits, failback window,
+ and abort thresholds before injecting a fault.
+4. **Isolate one origin without disabling it in Front Door.** Apply an approved reversible fault,
+ such as stopping the test Function App, and timestamp request and confirmation separately.
+ Keep the survivor unchanged. Do not weaken Easy Auth or ingress; probe health is not a security boundary.
+5. **Measure detection and survivor behavior.** Maintain the bounded combined-peak evaluation load
+ plus headroom. Capture every attempt, failure, timeout, latency, safe correlation ID, probe,
+ routing, and regional dependency signal. Missing telemetry is not success. Meet the full soak
+ criteria; evaluation-path results do not replace the separate live-path capacity evidence.
+6. **Restore on completion or abort.** Stop unsafe load and restore the saved state at the deadline,
+ any abort threshold, or completion. Cleanup must run even if assertions or the controller fail.
+ Escalate immediately if restoration cannot be confirmed.
+7. **Verify failback, then reverse direction.** Check the restored origin's dependencies, key and
+ credential resolution, readiness, and evaluation responses. Wait through the agreed stable
+ observation window before testing the other origin. Both directions must pass.
+8. **Record results and gaps.** Keep sanitized approvals, configuration/package identifiers,
+ workload limits, timestamps, rates, latency/errors, dependency evidence, aborts, and restoration.
+ Mark each criterion pass/fail/not tested with an owner and retest date. Never attach secrets,
+ bearer tokens, request bodies, OTPs, or nonces.
+
+
+### Additional validation
+
+Stopping a Function App tests routing when one application origin is unavailable. Add separate
+nonproduction tests for regional storage, vault, identity access, network paths, control plane,
+and telemetry. Capture evidence outside the affected region. Include cold startup, credential
+refresh, and restoration so the plan covers more than an already-warm app. Record any scenario
+that cannot be tested safely and assign an owner and follow-up date.
+
+Use separate approved checks for real Microsoft Entra caller timing, provider acceptance, and
+handset delivery. Define retry and availability targets with the provider and onboarding owner.
+Front Door and this runbook do not guarantee zero downtime or make uncertain retries safe.
## Observed failover results and limitations
@@ -435,14 +373,14 @@ We saw no failed samples before the outages or after restarting the Functions.
A failed request is not guaranteed to be replayed on another origin. The actual Entra caller's
handling of these errors was not tested.
-These trials didn't show a consistent benefit from 10-second probes or a third region.
-More regions can add capacity and help tolerate additional failures, but they don't remove routing
+These trials did not show a consistent benefit from 10-second probes or a third region.
+More regions can add capacity and help tolerate additional failures, but they do not remove routing
delays, shared provider dependencies, or application faults. Faster probes generate more traffic
and can make brief problems trigger routing changes sooner. Decide how much disruption your
application can accept, then repeat the tests from your callers' locations and at your expected load.
Do not add blind retries to live sends: a timeout can occur after provider acceptance, and the sample
-does not deduplicate deliveries. These results don't establish zero downtime, a production SLA,
+does not deduplicate deliveries. These results do not establish zero downtime, a production SLA,
or successful delivery to a phone.
## Operations and rollback
diff --git a/docs/GETTING-STARTED.md b/docs/GETTING-STARTED.md
new file mode 100644
index 0000000..bd4023a
--- /dev/null
+++ b/docs/GETTING-STARTED.md
@@ -0,0 +1,193 @@
+# Deployment guide
+
+
+
+[Documentation](README.md) | [Setup reference](../setup/docs/README.md) | [Troubleshooting](../setup/docs/Troubleshooting.md)
+
+Deploy **one regional endpoint** for Microsoft Entra ID SMS and voice requests. Guided setup
+publishes the selected package. Validate each channel you plan to use.
+
+Follow in order: [prepare](#1-get-ready), [register](#2-register-your-application),
+[deploy](#3-run-guided-setup), [connect and test](#4-connect-your-provider-and-test),
+then [activate and hand over](#5-activate-and-hand-over).
+Portal labels may differ from the screenshots.
+
+## 1. Get ready
+
+Start with a **separate test environment** in a nonproduction tenant and subscription. Create a
+new application registration and choose a unique resource prefix. This makes the endpoint easy
+to identify and keeps testing separate from production.
+
+Before purchasing or deploying, confirm access to a supported provider's EPP integration through
+[Microsoft Security Store](https://securitystore.microsoft.com/private-solutions) and arrange
+Microsoft's approved testing and activation procedure. See
+[access and support](ONBOARDING.md#before-purchasing-or-deploying).
+
+In Security Store, open **Private solutions** and select the approved subscription. Check
+[guided provider support](ONBOARDING.md#provider-credential-names).
+
+Use the [SMS cost estimation guide](COST-CALCULATOR.md) to create a planning estimate,
+then confirm destination rates, fees, and final costs with your provider.
+
+Prepare:
+
+- **Provider account:** complete account and sender registration for SMS or voice.
+- **Shell:** Azure Cloud Shell in PowerShell mode, or PowerShell 7+ with an Azure CLI user login.
+ Azure CLI 2.60.0+ is required for Flex Consumption (FC1), or 2.48.1+ for Premium (EP1).
+ Allow access to GitHub, Azure, Microsoft Graph, Key Vault, and the deployment endpoints.
+ C# also needs the .NET 8 SDK and NuGet access.
+- **Administrators:** Azure deployment and role-assignment access, a Privileged Role Administrator
+ for setup, and an Authentication Policy Administrator for activation.
+- **Azure region and budget:** check availability and subscription quota for your chosen Linux
+ plan. Azure resources and provider services can incur charges.
+
+Check the [full prerequisites](../setup/docs/README.md#prerequisites-for-step-2), including the
+Microsoft Entra ID allowed-tenants preview. Setup can install Bicep, but not Azure CLI.
+
+Assign an **onboarding owner** and review [production readiness](CONTRACT.md#production-limitations).
+
+## 2. Register your application
+
+In the customer tenant's **Microsoft Entra admin center > App registrations > New registration**:
+
+1. Give the application a recognizable name.
+2. Select **Accounts in this organizational directory only**. Leave the redirect URI blank,
+ then select **Register**. Setup will make the app organizational multitenant and restrict
+ its allowed tenants after you approve the deployment plan.
+3. On **Overview**, save the **Directory (tenant) ID** and **Application (client) ID** privately.
+ Use the client ID, not the Object ID.
+
+
+
+*Some portal versions label the account type **Single tenant only**. Select the customer directory.
+The example above is an unsaved form and cannot be reused.*
+
+Use these labels to find the IDs:
+
+
+
+*Use **Application (client) ID** for `-ApplicationId` and **Directory (tenant) ID** for `-TenantId`.
+The **Object ID** is not either of these setup inputs. All ID values are hidden in this example.*
+
+Let setup add the required API permission and app role. No client secret is needed.
+See [application registration details](../setup/docs/README.md#step-1---manually-create-the-application).
+
+Keep the Azure subscription ID handy. The [setup values worksheet](ONBOARDING.md#inputs-you-supply-to-setup)
+lists every input. Use a new app and prefix for an independent endpoint.
+
+## 3. Run guided setup
+
+After the checks above, open **PowerShell 7** in a private working folder and download the script:
+
+```powershell
+Invoke-WebRequest `
+ -Uri 'https://raw.githubusercontent.com/Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample/main/setup/Setup-Epp.ps1' `
+ -OutFile .\Setup-Epp.ps1
+```
+
+Review the downloaded script, then run it:
+
+```powershell
+.\Setup-Epp.ps1
+```
+
+On a shared workstation, use `.\Setup-Epp.ps1 -ForceAuthentication` for a fresh Azure CLI sign-in.
+
+Select the tenant, subscription, application **client ID**, and region. Then choose:
+
+- **Provider and route:** enter or confirm the provider by name. **Tenant scope** means Global/EU
+ provider routing, not Azure region or a data-residency guarantee.
+- **Platform:** JavaScript, C#, or Python.
+- **Hosting:** FC1 can scale to zero and cold-start; EP1 has paid warm capacity. FC1's usage grant
+ does not make deployment free. Review [plan details](../setup/docs/README.md#service-plan-selection).
+- **Resource prefix:** 2-8 lowercase letters/digits, starting with a letter.
+
+Complete required sign-in, MFA, or administrator consent. Resolve preflight errors before approval.
+
+For existing encryption credentials, follow
+[certificate selection](../setup/docs/README.md#encryption-certificate-selection). Coordinate
+changes across every endpoint sharing the application.
+
+**Review the complete deployment plan before typing `Yes`.** Check the accounts, region,
+SMS and voice provider URLs, platform, plan, cache settings, and resource names. Also check
+permissions, certificate key IDs, and access to any existing vault. The plan includes Microsoft Graph `Application.Read.All` for the Microsoft phone-provider service
+principal. `No` or Enter cancels deployment.
+
+**Plan review:** run interactively and choose **No** at the final prompt. See
+[plan review](../setup/docs/README.md#reviewing-the-plan-without-deploying).
+
+Wait for **Deployment completed** and the saved summary before Step 4. Setup creates the
+Function App, storage, Key Vault, identities, and monitoring as one coordinated deployment.
+Begin without precreating these resources so setup can apply the complete reviewed plan.
+Keep `epp-output` private. Its [summary](../setup/docs/README.md#read-the-deployment-summary)
+identifies the resources, endpoint, app, and encryption key. No private key is downloaded.
+
+Setup verifies the private-key reference and **Easy Auth** before opening public access. Keep
+Easy Auth enabled. If setup fails, use the
+[troubleshooting guide](../setup/docs/Troubleshooting.md) rather than deleting resources and starting over.
+
+To find the deployed resources, open **Azure portal > Resource groups** and select the group
+listed in your deployment summary.
+
+
+
+*This existing Premium test deployment shows the resource types to look for, not a new
+deployment or a guarantee that your resource list will be identical. Use setup rather than
+the portal's Create button to deploy the endpoint.*
+
+## 4. Connect your provider and test
+
+Next, complete [provider authentication](ONBOARDING.md#complete-provider-authentication).
+API-key integrations use the
+[setup-created vault and exact secret names](ONBOARDING.md#where-to-enter-api-key-credentials-in-key-vault).
+OAuth uses the provider-administrator handoff, not API-key secrets.
+Keep credentials out of source, screenshots, and shared logs.
+
+In your Function App, open **Settings > Authentication**. Check that authentication is
+**Enabled**, access is set to **Require authentication**, and unauthenticated requests receive
+**HTTP 401 Unauthorized**. These are checks, not instructions to change the approved caller.
+
+
+
+*The **Microsoft** identity provider on this page validates the caller. It is not the phone
+provider you selected for SMS or voice.*
+
+Complete the [approved endpoint checks](ONBOARDING.md#validate-the-deployed-endpoint):
+
+1. Confirm that missing or unauthorized caller credentials are rejected.
+2. Run an authorized **evaluation** to check authentication and decryption without a provider send.
+3. Run a separately approved **live test for each channel you intend to activate** and confirm
+ both provider acceptance and receipt of the SMS or voice call. Acceptance alone is not proof of delivery.
+4. Check that the request and relevant logs appear in Application Insights.
+
+Use Microsoft's approved caller and test procedure for these checks; an ordinary Azure CLI token
+cannot represent that caller. Activate the endpoint after every required check passes. If a live
+send times out, investigate its outcome before trying again because the provider may already have
+accepted it.
+
+## 5. Activate and hand over
+
+Have an **Authentication Policy Administrator** follow the
+[supported activation procedure](../setup/docs/README.md#step-3---manually-validate-and-activate-policy).
+They save the existing policy, apply the approved endpoint URL and application client ID,
+preserve other settings, and validate the result.
+
+Repeat for each authentication method you intend to enable. SMS and voice can share the same
+Function endpoint URL and application client ID; no additional setup run is needed for the second channel.
+
+Before handing over, confirm:
+
+- [ ] Provider authentication, authorized testing, and recipient delivery are verified.
+- [ ] Policy is backed up, activated, and checked, with a [rollback plan](../setup/docs/README.md#rollback-and-decommissioning).
+- [ ] [Monitoring and alert notifications](MONITORING.md) work for your chosen runtime.
+- [ ] Owners are assigned for provider credentials, [certificate renewal](../setup/docs/README.md#encryption-certificate-lifecycle),
+ support, and costs.
+
+Setup creates telemetry resources, **not alert rules or renewal notifications**.
+Keep the deployment summary and validation evidence in restricted storage.
+Deleting Azure resources does not undo an authentication policy change.
+
+## Find more detail
+
+Use the [task-based documentation index](README.md) for operations, developer references,
+and advanced topics. For a failed step, go directly to [troubleshooting](../setup/docs/Troubleshooting.md).
diff --git a/docs/LOGGING.md b/docs/LOGGING.md
new file mode 100644
index 0000000..3cec701
--- /dev/null
+++ b/docs/LOGGING.md
@@ -0,0 +1,120 @@
+# Application log reference
+
+[Documentation](README.md) | [Find deployed logs](MONITORING.md#runtime-specific-log-discovery) | [HTTP contract](CONTRACT.md)
+
+Use this reference with the [monitoring walkthrough](MONITORING.md).
+
+**Exclude request bodies, decrypted fields, credentials, nonces, raw exceptions, and provider
+responses.** Keep body tracing off.
+
+## Runtime formats
+
+| Runtime | Emission and context |
+| --- | --- |
+| JavaScript | JSON service records with immutable request context and existing camelCase/`x-ms-*` fields. |
+| Python | Standard `logging`; `otp_log.py` defines fixed event IDs, names, levels, and fields. An immutable `LoggerAdapter` supplies Function and Microsoft trace identifiers. |
+| .NET | Standard `ILogger`; `OtpLog` defines source-generated IDs/names. `ILogger.BeginScope` supplies `FunctionName`, `FunctionRequestId`, `FunctionInvocationId`, `MsClientRequestId`, `MsCorrelationId`, and `MsCorrelationIdSource`. |
+
+Each invocation ends with `request_completed`. Logger/exporter behavior controls formatting and
+property preservation. JavaScript, Python, and .NET field names differ; verify the
+[deployed schema](MONITORING.md#runtime-specific-log-discovery).
+
+## Event sequence
+
+A successful live request emits these events in order:
+
+| Service event | Safe information recorded |
+|---|---|
+| `request_received` | Function invocation and available raw Microsoft trace IDs under their `x-ms-*` names; no raw body or arbitrary headers. |
+| `envelope_validated` / `payload_validated` | Allowlisted body metadata: validated payload type, normalized `channel`, `evaluation` and optional `ttlSeconds`. |
+| `delivery_context_decrypted` | Decryption completed; no plaintext fields, JWE or key ID. |
+| `provider_selected` | Registered provider and its authentication mode. |
+| `provider_credential_resolution_started` | OAuth client-assertion or Key Vault credential source, with explicitly named raw OAuth application/identity/tenant IDs. |
+| `provider_credential_resolved` | Credential resolution and usability checks completed, with elapsed time; no secret, token, assertion or claims. |
+| `provider_request_build_started` | The provider is preparing the outbound request. |
+| `provider_request_built` | Allowlisted HTTP method, final endpoint scheme/host/port/API path, HTTPS and disabled redirects; no query string, authorization headers or body. |
+| `provider_request_started` | The outbound send is beginning, with method, sanitized endpoint and timeout. |
+| `provider_response_received` | Actual upstream HTTP status; emitted before response-body reading completes. |
+| `provider_response_processed` | Mapped provider status/outcome, fixed failure classification and duration. Raw provider descriptions and bodies are excluded. |
+| `response_prepared` | Response status and booleans indicating nonce/correlation inclusion, not their values or the response body. |
+| `request_completed` | Final HTTP status, result, and elapsed time; stage-specific fields stay on their events. |
+
+JavaScript uses `envelope_validated`; Python/.NET use `payload_validated`.
+Evaluation skips provider work and emits `evaluation_completed`, `response_prepared`, and
+`request_completed`.
+
+Failures emit `request_failed` with stage, reason, and HTTP status (`FailureStage` and
+`FailureReason` in .NET). Provider results use
+`Outcome` and a safe classification: `provider_http_error`, `provider_rejected`, `unrecognized_provider_status`,
+`missing_provider_message_id`, or `invalid_provider_json`. `provider_response_processed` includes
+that classification, upstream HTTP status, and normalized status, never descriptions/bodies.
+Credential acquisition/refresh failures use `credential_refresh_failed`; unexpected failures use
+`unexpected_error`. No event adds credential reads, acquisition, or send retries.
+
+`response_prepared` runs before successful and failed returns; it does not prove caller receipt.
+
+`providerHttpStatus` is recorded when response headers arrive.
+An upstream `200` can precede a body-read timeout: `provider_response_received` may be followed by
+`request_failed` with Function `504`, without mapped provider status or success acknowledgement.
+Unknown status text and malformed JSON are never logged; malformed JSON emits only
+`provider_response_invalid_json` before provider outcome rules run.
+
+## Safe fields and identifiers
+
+Build metadata from **validated allowlisted fields**. Exclude unknown, tenant, locale, risk, and
+decrypted data.
+
+`providerCredentialSource` is `managed_identity_client_assertion` for app tokens and `key_vault`
+for API keys. Cached resolution does not imply a network fetch or fresh token exchange.
+`providerCredentialElapsedMs` includes resolution and usability checks, not a separate deadline.
+SDK diagnostics remain suppressed.
+
+`provider_request_built` describes the outbound request, not a new physical connection or HTTP
+client. `providerEndpoint` records the **final** scheme, host/port, and path, which can differ
+from the configured base URL. For example, `https://provider.example/epp/voice?key=secret` becomes
+`https://provider.example/epp/voice`. Userinfo, query strings, and fragments are excluded, but
+**path segments are not redacted**: use provider-approved paths without secrets/personal data.
+Logging does not alter the actual URL or required query parameters. Standard verbs are uppercase;
+custom/invalid verbs appear as `other` without changing the sent request.
+
+JavaScript/Python events carry Function request/invocation IDs and Microsoft trace IDs available
+at that stage. JavaScript also carries known channel/evaluation/provider context; Python uses
+its adapter and event arguments. Initially only header IDs are available; a valid envelope can
+later supply the selected correlation. The generated Function request ID joins events even when
+Microsoft IDs are absent or change. Log names deliberately distinguish these sources:
+
+| Log field | Source and meaning |
+|---|---|
+| `functionName` | Runtime function name: `SendOtp` in JavaScript/.NET, `send_otp` in Python. The HTTP route remains `/api/SendOtp` in every runtime. |
+| `functionRequestId` | Generated by this handler. Matches `requestId` in failure responses; it is not Microsoft's request ID. |
+| `functionInvocationId` | Azure Functions host invocation ID. Null for direct handler calls without a host context. |
+| `x-ms-client-request-id` | Raw Microsoft per-attempt ID from the header of the same name, not the generated fallback used by dispatch. |
+| `x-ms-correlation-id` | Raw selected Microsoft correlation: envelope `correlationId` first, then the header of the same name. The existing precedence is unchanged, and this never contains a generated Function ID. |
+| `msCorrelationIdSource` | `envelope`, `header` or `none`; disambiguates the selected value when the envelope and header differ. |
+| `providerMessageId` | Raw provider-normalized message/reference ID returned by the provider, including Telesign `reference_id`, for support lookup. Not filled from delivery or Function IDs, although a provider may echo an ID it received. |
+| `providerTenantId` | Raw configured `EPP_PROVIDER_TENANT_ID` for OAuth, not incoming envelope `tenantId`. |
+| `functionOutboundClientId` | Raw application's `EPP_OUTBOUND_CLIENT_ID` used for provider access, not a Microsoft request ID. Guided setup reuses the endpoint app client ID here; its outbound purpose is distinct from the inbound audience setting. |
+| `functionOutboundManagedIdentityClientId` | Raw configured `EPP_OUTBOUND_MI_CLIENT_ID` used to obtain the app assertion, not the Key Vault identity or the identity's principal/Object ID. |
+| `providerEndpoint` | Validated final outbound URL without userinfo, query string or fragment; scheme, host/port and API path remain visible. |
+
+The six external/configuration ID fields preserve raw values and case, without hashing/truncation.
+Only nonblank strings of **1-128 ASCII characters** are recorded: a letter/digit first, then
+letters, digits, `.`, `_`, `:`, or `-`. Missing/blank values are null; other invalid values are
+null and their names appear in `omittedIdFields`, never their values.
+These guards affect logs only, not wire IDs, correlation precedence, or outcomes.
+Application/client/tenant IDs are identifiers, not secrets or trust assertions; incoming
+`tenantId` never becomes a trusted identity.
+
+## Levels and collection
+
+Normal/completion events use **Information**; invalid requests, non-success 4xx outcomes, and
+advisories use **Warning**; 5xx failures/timeouts use **Error**.
+Keep Information logs enabled for investigation without enabling SDK/body tracing.
+
+Filters and sampling can remove events. Application events are logs, not host Request telemetry;
+excluding `Request` from sampling does not retain every event. Configure collection and retention
+deliberately. Easy Auth rejection happens before the handler: inspect platform/authentication
+diagnostics and caller observations rather than expecting an application event.
+
+Next: [verify collection](APPLICATION-INSIGHTS.md#single-region-configuration), then use
+[runtime-specific queries](MONITORING.md#runtime-specific-log-discovery).
diff --git a/docs/MONITORING.md b/docs/MONITORING.md
index 54f0f58..cebb653 100644
--- a/docs/MONITORING.md
+++ b/docs/MONITORING.md
@@ -1,14 +1,15 @@
-# Monitoring setup and sample queries
+# Monitoring
-Use this guide to find your endpoint's logs and set up alerts. Guided setup creates Application
-Insights and a Log Analytics workspace, but **you still need to configure and test notifications**.
+
-For **tenant authentication outcomes and potential abuse signals**, use the separate
-[Microsoft Entra sign-in monitoring guide](SIGN-IN-MONITORING.md). Entra log export and
-sign-in alerts are not configured by endpoint setup; sign-in failures are not automatically
-provider failures or evidence of OTP delivery.
+[Documentation](README.md) | [Collection diagnostics](APPLICATION-INSIGHTS.md) | [Log schema](LOGGING.md) | [Endpoint troubleshooting](../setup/docs/Troubleshooting.md#after-deployment-triage)
-For your first single-region deployment:
+Guided setup creates Application Insights and Log Analytics. Use this guide to query them and
+configure alerts.
+
+For identity signals, use [Microsoft Entra ID sign-in monitoring](SIGN-IN-MONITORING.md).
+
+For a first single-region deployment:
1. [Find the monitoring resources and confirm data is arriving](#1-record-the-deployment-and-confirm-telemetry)
after an [authorized test](ONBOARDING.md#validate-the-deployed-endpoint).
@@ -17,15 +18,15 @@ For your first single-region deployment:
3. [Set up and test alerts](#5-set-up-notifications-and-alert-rules), including certificate
reminders and cost ownership.
-The multi-region queries, shared Workbook, and Front Door sections are optional.
-You do not need them to find single-region logs. For collection and identity details, see
-[Application Insights](APPLICATION-INSIGHTS.md).
+For missing data, use [collection checks](#9-sampling-ingestion-gaps-privacy-and-validation).
+See also [multiple regions](#3-multiple-regions-and-cross-workspace-queries),
+[Workbook](#6-build-a-shared-operations-workbook), [availability checks](#7-availability-authenticated-functions-and-fc1),
+and [Front Door](#8-optional-azure-front-door-layer).
-These examples do not create monitors automatically. Review permissions, privacy, and charges
-before applying them. Thresholds and intervals are **starting points, not SLAs**; tune them to
-your deployment.
+Review permissions, privacy, and charges. Tune sample thresholds to the deployment.
-## 1. Record the deployment and confirm telemetry
+
+## 1. Record the deployment
Keep a private inventory with one row per expected regional endpoint:
@@ -36,23 +37,19 @@ Keep a private inventory with one row per expected regional endpoint:
| Observed `AppRoleName` and `AppRequests.Name` | Filter this Function and its `SendOtp` request operation without mixing other apps or readiness probes. |
| Runtime, hosting plan, deployment version, and owner | Explain schema differences, cold starts, releases, and escalation. |
| Expected traffic or authorized synthetic-test cadence | Decide whether missing data is actionable, including standby origins. |
-| Optional Front Door profile, endpoint, origin group, and origin | Associate routing and health with the regional Function. |
+| Front Door profile, endpoint, origin group, and origin | Associate regional routing and health with the Function. |
-Do not derive the expected inventory solely from recent telemetry: an origin that
-never reports would be absent from that list.
+Build the inventory from the deployment record, not recent telemetry.
-The [guided setup](../setup/docs/README.md) configures workspace-based Application
-Insights, an ingestion identity/role, and Function diagnostic settings. Inspect the
-actual deployment rather than assuming that configuration succeeded. Other deployment
-methods can differ. The current setup template requests 30-day retention; confirm
-effective workspace/table retention and organizational policy before changing it.
-It does not create the action groups, Workbooks, or alert rules described here.
+Guided setup does not create action groups, Workbooks, or alerts. Verify
+[collection](APPLICATION-INSIGHTS.md#single-region-configuration) and effective retention.
-### Find the linked resources in the portal
+
+### Portal resources
1. Open the Function App named in your deployment summary.
-2. Select **Monitoring > Application Insights**. Follow the linked resource name; do not
- select **Change your resource** or **Apply** just to view your logs.
+2. Select **Monitoring > Application Insights** and follow the linked resource name.
+ Viewing logs does not require **Change your resource** or **Apply**.

@@ -61,24 +58,15 @@ It does not create the action groups, Workbooks, or alert rules described here.

-These are real portal views of an existing test deployment, with identifying information and
-connection details permanently hidden. Resource names, regions, and portal layouts may differ.
-The links show where to navigate; they do not prove that test traffic or delivery succeeded.
-
-In the Azure portal, inspect each Function's Application Insights association,
-monitoring settings, managed-identity ingestion authorization, and diagnostic
-destinations. Do not expose connection strings or credential values. Application
-Insights telemetry and resource diagnostic logs are different pipelines:
-`AppRequests`/`AppTraces` are not interchangeable with `FunctionAppLogs` or
-`AzureDiagnostics`. Enabling diagnostic logs alone does not prove request telemetry
-is arriving, and platform metrics do not require log export to appear in Metrics.
-
-Use **Log Analytics workspace > Logs** for all KQL below. Application Insights'
-application-context query experience can instead use names such as `requests`,
-`traces`, `timestamp`, and `cloud_RoleName`; do not mix that schema with these
-workspace examples. Replace every angle-bracket placeholder with your private
-inventory value before running. `_ResourceId` in these application tables identifies
-the **Application Insights component**, not the Function App.
+Screenshots hide identifiers. Portal names and layout may differ.
+
+Keep connection strings private. `AppRequests`/`AppTraces` are not interchangeable with
+`FunctionAppLogs` or `AzureDiagnostics`.
+See [pipelines and table names](APPLICATION-INSIGHTS.md#signals-tables-and-metrics-are-different-things).
+
+Run the KQL below in **Log Analytics workspace > Logs** and replace every placeholder.
+Application Insights uses a different table schema. `_ResourceId` identifies the Application
+Insights component, not the Function App.
Run this discovery query after a controlled authorized test. The examples use
`SendOtp` as the request name; replace that literal everywhere if your observed
@@ -92,12 +80,11 @@ AppRequests
| order by LastSeen desc
```
-Read access is required on every workspace queried. Give operators only the
-necessary resource/workspace access; saving a Workbook does not grant its readers
-access to its data. Cross-workspace alert evaluation also needs access to each
-workspace under the rule's configured identity.
+Grant only required read access on **each workspace**. Workbook sharing does not grant data access;
+alert evaluation needs its own identity's access, not the author's.
-### Run a first query and read the results
+
+### First query
1. In your workspace's **Logs**, close any welcome or Queries hub dialog. If the portal opens
in **Agent** mode, switch it off for this walkthrough, then choose **KQL mode**.
@@ -150,16 +137,10 @@ AppRequests
| order by TimeGenerated asc
```
-`ItemCount` accounts for the telemetry items represented by a sampled record.
-Weighted counts and percentiles remain estimates when sampling is active; they
-cannot recover telemetry that was never ingested. Empty buckets are **no data**,
-not zero failures or 100% availability. `Success` is the instrumented request
-outcome, not evidence that a phone received an OTP.
-
-Both evaluation and live calls can return HTTP 200. These request-table statistics
-combine them unless your deployed instrumentation provides a verified classification.
-Use the JavaScript service-event query below for a separate view; do not infer
-live delivery from the HTTP status.
+`ItemCount` weights sampled records; counts/percentiles remain estimates, not recovery of lost data.
+Empty buckets mean **no data**, not zero failures or availability. `Success` is a request outcome,
+not handset receipt. HTTP 200 includes both evaluation and live calls; use
+[classified service events](#separate-evaluated-accepted-and-failed-completions) where verified.
### Failure status breakdown
@@ -270,26 +251,17 @@ required; the request query is not a synthetic scheduler.
## 4. JavaScript service-event queries
-The JavaScript implementation emits JSON messages with `logType: "service"` and
-`eventName` through its invocation logger. The following examples assume those
-messages arrive as JSON in `AppTraces.Message`. They are **not portable event
-parsers for every runtime**:
-
-- JavaScript and Python `host.json` enable Application Insights sampling and exclude
- `Request` telemetry from that host sampling. Trace events can still be sampled
- or filtered. The setting is not a guarantee against ingestion loss.
-- Python uses logging messages plus `extra` fields such as `event_name`, not this
- JavaScript JSON contract. Whether those fields reach custom properties depends
- on the deployed logging pipeline.
-- .NET isolated uses `ILogger` events and declares OpenTelemetry in `host.json`.
- Inspect the actual host/worker exporter configuration, tables, and properties;
- do not assume JavaScript JSON or complete worker telemetry.
+These queries require JavaScript invocation logs containing `logType: "service"` and `eventName`
+as JSON in `AppTraces.Message`. **Do not apply this parser to Python or .NET**; use
+[runtime-specific discovery](#runtime-specific-log-discovery).
-Verify a controlled request appears in `AppRequests` and that its expected events
-appear in the deployed trace schema before relying on any service-event alert.
-An empty JSON-filtered result can mean a parser/pipeline mismatch.
+Verify a controlled request and its events before creating alerts. Traces can be sampled or
+filtered, and an empty JSON result can mean a parser/pipeline mismatch. Check
+[runtime collection defaults](APPLICATION-INSIGHTS.md#what-the-checked-in-sample-configures)
+and [sampling limits](APPLICATION-INSIGHTS.md#sampling-privacy-and-collection-limits).
-### Separate evaluated, accepted, and failed completions
+
+### Evaluation and delivery outcomes
```kusto
AppTraces
@@ -407,18 +379,16 @@ AppTraces
| summarize StoredRows = count() by SchemaStatus, Result, HttpStatus
```
-These discovery counts are **stored rows**, not sampled traffic totals or delivery receipts.
-Do not turn them into alerts until the selected release's schema and collection are verified.
-In particular, the .NET host's OpenTelemetry declaration is not an initialized direct worker
-exporter. Missing worker events require a collection investigation, not a green application
-health result. Malformed .NET request binding can fail before the handler emits any events.
+These are **stored rows**, not traffic totals or delivery receipts. Verify the release's schema and
+collection before creating alerts. Missing events require investigation, not a healthy status:
+.NET host OpenTelemetry does not initialize a direct worker exporter, and malformed .NET binding
+can fail before handler events. See [collection configuration](APPLICATION-INSIGHTS.md#what-the-checked-in-sample-configures).
-For an incident, use the host `OperationId`/invocation ID and whichever application request ID
-is actually exported. Keep raw Microsoft/provider support IDs distinct; not every runtime
-exports the provider reference. Never use OTPs/nonces or message bodies as correlation keys.
-Look for a correlated `request_failed` event's stage/reason and endpoint status, then the
-separate provider-response event's upstream status. A provider `200` can precede a body-read
-timeout and Function `504`; it is not necessarily a successful invocation.
+For incidents, correlate `request_failed` stage/reason/status with provider-response events.
+Use the exported host `OperationId`/invocation and application request IDs, not OTPs, nonces, or bodies.
+Keep Microsoft/provider IDs distinct; some runtimes omit provider references.
+An upstream `200` can precede a body-read timeout and Function `504`.
+See [event order and identifier provenance](LOGGING.md).
`credential_refresh_failed` also differs operationally: JavaScript/Python have a polling
refresh loop, while .NET emits this event on failed startup/cache-fill acquisition and has no
@@ -428,22 +398,17 @@ health check.
## 5. Set up notifications and alert rules
-Have an operator review and create these resources in a nonproduction environment
-first. Rule creation requires appropriate write permissions; data access alone is
-not enough. Agree on ownership, severity, escalation, and maintenance handling.
+Start in nonproduction with an operator who has rule-write permissions, not just data access.
+Agree ownership, severity, escalation, and maintenance handling.
### Action groups
-In **Azure Monitor > Alerts > Action groups > Create**, select the resource group,
-name, and processing region appropriate to your residency policy. Add an on-call
-email receiver and, if required, a secured incident-management integration. Prefer
-a notification path that does not depend on the OTP provider being monitored.
-Use the common alert schema where supported and send a test notification before
-attaching production rules. A test notification verifies routing, not that an
-alert condition actually evaluates correctly.
+In **Azure Monitor > Alerts > Action groups > Create**, choose the resource group, name, and
+residency-appropriate processing region. Add on-call email and any secured incident integration;
+avoid depending on the monitored OTP provider. Use the common alert schema where supported.
+Test notification routing before attaching production rules; that test does not verify the alert query.
-For multi-region deployments, decide whether the action group should use global
-or supported regional processing, and ensure an incident in one app region cannot
+For multiple regions, choose global/supported regional processing so one failed app region cannot
disable the only response path. See [action group setup][action-groups].
### Metric alerts
@@ -457,7 +422,7 @@ aggregation, window, evaluation frequency, dimensions, and action group.
As a starting point, where the selected resource exposes the HTTP 5xx metric,
alert on **Total > 5 over 5 minutes**, evaluated every minute. Use Application
Insights request failure rules below when that platform signal is unavailable or
-when you need the `SendOtp` operation filter. Add plan-appropriate execution,
+when the `SendOtp` operation filter is required. Add plan-appropriate execution,
memory, or instance signals only after establishing a baseline; there is no
single CPU/instance threshold appropriate to both FC1 and EP1.
@@ -468,20 +433,17 @@ throttling, storage availability/latency, and credential/certificate expiry thro
their respective resource monitoring. A successful Function response alone does
not validate those dependencies' future readiness.
-### Certificate, credential, and cost ownership
+
+### Operational ownership
-Keep the setup summary's `certificateExpiresUtc`, vault certificate/secret versions, and Entra
-encryption credential expiry in a restricted inventory with a renewal owner and escalation backup.
-For certificates issued by setup, configure vault-wide certificate contacts and retain the
-`EmailContacts` lifetime action. For a reused certificate, coordinate its existing issuance policy
-and notifications with the source-vault owner. Follow the
-[renewal runbook](../setup/docs/README.md#encryption-certificate-lifecycle).
-Contacts alone are not automatic renewal, and a new vault certificate version does not update
-the Function's version-pinned reference or Entra by itself.
+Keep `certificateExpiresUtc`, vault certificate/secret versions, and Entra encryption credential
+expiry in a restricted inventory with a renewal owner and backup. Follow the
+[renewal runbook](../setup/docs/README.md#encryption-certificate-lifecycle) for vault-wide contacts,
+`EmailContacts`, and reused-certificate policies. **Contacts do not renew certificates; a new vault
+version does not update the Function's pinned reference or Entra.**
-Create an independent reminder before the certificate's 30-day near-expiry window and rehearse
-the approved single-certificate renewal/replacement procedure in nonproduction. If your operations
-process uses Key Vault near-expiry/new-version events, an operator must configure an
+Set an independent reminder before the 30-day near-expiry window and rehearse approved renewal
+in nonproduction. For Key Vault near-expiry/new-version events, configure an
[Event Grid subscription and handler](https://learn.microsoft.com/azure/key-vault/general/event-grid-overview)
and test its notification path. Diagnostic settings alone do not create these alerts, and those
events do not automatically become Application Insights traces.
@@ -504,7 +466,7 @@ condition and action group, and save only after reviewing estimated charges.
Use an explicit query lookback consistent with the query's time filter.
For the request summary examples, change the filter to `ago(5m)` in every source
-and remove `bin(TimeGenerated, 5m)` from the grouping when you want **one summary
+and remove `bin(TimeGenerated, 5m)` from the grouping for **one summary
row per origin over the whole rolling window**. This avoids evaluating a partial
calendar bucket as if it were a complete five-minute interval. For the single
region query, remove the grouping entirely. Remove the final time sort in both
@@ -551,21 +513,18 @@ Add query panels using the correct workspace scope:
that parser is verified; otherwise show the runtime-specific validated view.
5. Relevant Function/dependency metric charts, current alerts, and deployment context.
-For one region, select one workspace/component/role. For several regions, use
-explicit workspace sources or a carefully scoped shared workspace and retain the
-regional breakdown. Add time/resource parameters for interactive investigation;
-replace hardcoded query windows when wiring a time parameter so it actually
-controls every source. Keep scheduled-alert queries independent of Workbook-only
-parameters. Do not color an empty panel green.
+Scope to the chosen workspace/component/role; multiple regions need explicit sources or scoped
+shared-workspace queries with regional grouping. Bind time/resource parameters into every source,
+replacing hardcoded windows. Scheduled alerts cannot depend on Workbook-only parameters.
+**Do not color an empty panel green.**
-Save to an appropriate resource group, grant Workbook access to the operations
-team, and verify readers also have data access. Keep private resource identifiers,
-query exports, and screenshots out of public documentation. See
-[Workbook authoring][workbooks].
+Save to the approved resource group and verify both Workbook and data access for readers.
+Keep identifiers, exports, and screenshots private. See [Workbook authoring][workbooks].
-## 7. Availability, authenticated Functions, and FC1
+
+## 7. Availability monitoring
-**Never exempt `SendOtp` from Easy Auth or make it publicly anonymous for probes.**
+Keep `SendOtp` protected by Easy Auth for every probe and test.
An unauthenticated 401/403 demonstrates rejection, not a successful authorized
evaluation or provider call. Standard availability tests do not perform the
complete EPP token acquisition, encrypted-request creation, and nonce-validation
@@ -581,8 +540,8 @@ For the actual authentication/decryption path, use the supported EPP test
procedure and an authorized caller. An independently operated synthetic runner
must be able to satisfy existing caller restrictions, acquire fresh credentials,
construct a valid encrypted evaluation request, and verify the nonce without
-recording it. If that caller capability is unavailable, record the monitoring
-gap; do not broaden Easy Auth allowlists just to make testing convenient.
+recording it. If the runner cannot meet those caller requirements, document the
+monitoring gap and retain the approved Easy Auth allowlist.
Record sanitized success/duration results through your approved monitoring
pipeline and monitor the runner's schedule/failures separately.
@@ -606,9 +565,11 @@ healthy but unused. Do not open origin ingress to bypass its access restrictions
Review the current [availability test capabilities][availability] before choosing
Standard tests or a custom authenticated runner.
-## 8. Optional Azure Front Door layer
+
+
+## 8. Front Door monitoring
-Only add this layer if using [manual Front Door onboarding](FRONTDOOR.md).
+Use this layer with the [Front Door multi-region design](FRONTDOOR.md).
For Standard/Premium, inspect the profile's Metrics for **Origin Health Percentage**,
**Origin Latency**, **Request Count**, and response/error signals supported by the
selected metric namespace. Confirm units and available origin/origin-group
@@ -634,13 +595,14 @@ as edge observations. Front Door probes do not perform an authenticated encrypte
`SendOtp` evaluation or prove provider delivery. A probe path must follow the
separate readiness design described above. See [Front Door monitoring][front-door].
-## 9. Sampling, ingestion gaps, privacy, and validation
+
+## 9. Sampling and data quality
-If data stops arriving, check the time range and workspace/component/role filters,
-actual traffic, deployment changes, log levels, host/worker exporter configuration,
-ingestion authorization/network access, sampling, throttling, and workspace daily
-caps. Retention and collection transformations can also affect query results.
-Never interpret an inaccessible workspace or missing table as a healthy empty window.
+If data stops arriving, first check the time range, workspace, component, role filters,
+and actual traffic. Then check recent deployments, log levels, host and worker exporters,
+ingestion access, network access, sampling, throttling, and workspace daily caps.
+Retention and collection transformations can also affect query results. Treat an
+inaccessible workspace or missing table as a collection issue, not a healthy empty window.
This query estimates the event-to-ingestion gap for **received** requests:
diff --git a/docs/MULTI-PROVIDER-IMPLEMENTATION.md b/docs/MULTI-PROVIDER-IMPLEMENTATION.md
index c7f4a8b..34fa0f2 100644
--- a/docs/MULTI-PROVIDER-IMPLEMENTATION.md
+++ b/docs/MULTI-PROVIDER-IMPLEMENTATION.md
@@ -1,23 +1,27 @@
-# Add conservative primary-to-secondary provider fallback
+# Provider fallback
-You can adapt this application to try a secondary provider **only when the primary is known
-not to have accepted the message**. Keep one URL for Microsoft Entra ID, `/api/SendOtp`, with a primary
-account and a secondary account for the same SMS channel. You choose their order according to
-your approved account policy; these roles do not imply a vendor ranking or recommendation.
-The caller does not select an account or supply a provider header.
+
-The sample still ships with one provider per deployment and **no provider fallback**. This guide
-describes customer code you must implement and review. A timeout, lost response or generic
-provider error is not proof of nonacceptance: in those cases, do not automatically send again.
+[Documentation](README.md) | [Normal deployment](GETTING-STARTED.md) | [Provider interface](BRING-YOUR-OWN-PROVIDER.md)
-For a normal deployment, follow the [main setup guide](../README.md) instead. This is an advanced
-developer walkthrough: it requires custom code, durable operation state, provider-specific
-approval, and separate testing. It is not another step in guided onboarding.
+Add controlled primary-to-secondary fallback around `/api/SendOtp`. Keep one server-owned SMS
+provider order with durable state and provider-specific approval.
-## 1. Set a fixed provider order and a deny-by-default policy
+Use the secondary **only after proof that the primary did not accept the request**. Timeouts,
+lost responses, and generic errors are uncertain and must not trigger an automatic resend.
-Keep the existing HTTP registration in [SendOtp.js](../javascript/src/functions/SendOtp.js).
-Define the provider order in server-owned startup configuration:
+Follow [policy](#1-set-a-fixed-provider-order-and-a-deny-by-default-policy),
+[account configuration](#2-configure-and-prevalidate-both-accounts),
+[credential isolation](#3-give-each-context-its-own-credential-service-and-lifecycle),
+[durable state](#4-add-durable-operation-state-before-enabling-fallback),
+[fallback flow](#5-integrate-one-guarded-fallback-into-the-handler),
+[end-to-end deadline](#6-enforce-one-aggregate-deadline), and [validation](#7-validate-offline-then-deploy-only-after-separate-approval).
+[.NET/Python pointers](#net-and-python-integration-pointers) are not implemented fallback.
+
+
+## 1. Provider order
+
+Keep [SendOtp.js](../javascript/src/functions/SendOtp.js) and define the order at startup:
```javascript
const providerOrder = Object.freeze({
@@ -27,9 +31,8 @@ const providerOrder = Object.freeze({
});
```
-Reject unsupported channels and invalid configuration. Do not accept provider IDs, endpoints,
-vaults or scopes from request headers or bodies. There is no round-robin selection, fan-out,
-recursive fallback or primary retry. The secondary can be attempted at most once.
+Reject unsupported channels and request-supplied provider configuration. Do not use round-robin,
+fan-out, recursive fallback, or primary retries. Attempt the secondary at most once.
Keep **routing order** separate from **fallback eligibility**. Use this policy:
@@ -42,20 +45,15 @@ Keep **routing order** separate from **fallback eligibility**. Use this policy:
| Primary-only credential acquisition fails before transport is entered | Eligible only if this pre-dispatch fact is recorded, the account policy explicitly permits it, the durable operation guard allows the transition, and the request budget is sufficient. |
| Provider-specific, documented proof that the primary did not accept the operation | Eligible only through a reviewed nonacceptance classifier, explicit account policy, the durable guard and sufficient budget. |
-**Default to no fallback.** No real provider response is designated safe to fall back
-from by this guide. Obtain and review provider-specific semantics before enabling that path.
-An expired/rejected inbound token never reaches it. A provider credential failure must not bypass
-account suspension, consent requirements, entitlement restrictions or a provider block.
+**Enable fallback only after the provider confirms a safe nonacceptance signal.** Caller
+authentication and provider account controls still apply.
## 2. Configure and prevalidate both accounts
[config.js](../javascript/src/functions/config.js) accepts `readConfig(settings)`, so each
account can use its own immutable settings without changing `process.env` per request.
-The following is a **customer-owned configuration format**, not a file the sample loads.
-The account IDs are role labels. Replace each `` with the adapter selected
-for that account from the [existing lookup](../javascript/src/functions/providers/index.js);
-placeholders are not valid runtime provider names. This example uses API-key authentication;
-each account's settings must match its selected adapter's actual credential requirements.
+The following **customer-owned format** is not loaded by the sample. Replace each
+`` with a registered adapter and match its credential requirements.
```json
[
@@ -87,9 +85,9 @@ This SMS-only policy requires `EPP_PROVIDER_SMS_ENDPOINT` for each account. A vo
`EPP_PROVIDER_VOICE_ENDPOINT` instead. The validated request channel selects the matching endpoint;
there is no shared or other-channel endpoint fallback.
-Your startup loader must reject duplicate/missing IDs, missing endpoints for the policy's channel,
-mismatched authentication modes, unapproved endpoints, missing credential references and invalid
-deadline policy. Validate both contexts before enabling the endpoint, not only after the primary fails.
+At startup, validate both account configurations before enabling the endpoint. Check for unique
+IDs, the required channel endpoint, matching authentication mode, approved URLs, credential
+references, and a valid deadline policy.
For each selected [adapter](../javascript/src/functions/providers), inspect `credentialSpec`
for the required secret names and any account identifier, and `createRequest` for endpoint and
@@ -101,13 +99,14 @@ Set an account/destination-approved sender where required; an adapter's default
proof of approval. These requirements apply equally to primary and secondary roles.
Store no credential values in the configuration. Attach each referenced user-assigned managed
-identity to the Function App and grant the vault-reading identity scoped secret-read access,
-such as Key Vault Secrets User under RBAC or the approved access-policy equivalent. If using
+identity to the Function App. Grant the vault-reading identity scoped secret-read access, such
+as Key Vault Secrets User under RBAC or the approved access-policy equivalent. If using
the system-assigned identity, grant that identity instead. Client-ID settings do not attach
identities or grant access. Complete provider account, recipient, sender and consent prerequisites;
see [provider onboarding](ONBOARDING.md#complete-provider-authentication).
-## 3. Give each context its own credential service and lifecycle
+
+## 3. Credential isolation
Open [credentials.js](../javascript/src/functions/credentials.js). When caching is enabled, its
singleton retains the first selected configuration; passing another account does not switch that cache.
@@ -160,13 +159,13 @@ An evaluation request must not start credential acquisition;
already-running refresh is independent. Rebuild contexts on controlled restart when configuration
changes; never repurpose a live service for another account.
-## 4. Add durable operation state before enabling fallback
+
+## 4. Operation state
-The repository has **no durable attempt store or idempotency contract**. Implement these before
-adding a second possible submission. Agree on a stable, authorized logical-operation key with
-the caller/test contract. Do not assume a generated invocation ID, correlation GUID or nonce is
-an idempotency key. Bind the key to the authorized caller, operation and immutable request/policy
-identity so another request cannot reuse it to obtain a different delivery.
+Add a **durable attempt store and idempotency contract** before allowing a second submission.
+Agree on a stable, authorized operation key with the caller and test owner. Generated invocation
+IDs, correlation GUIDs, and nonces serve other purposes. Bind the operation key to the authorized
+caller, request, and fixed policy so another request cannot reuse it for a different delivery.
Use transactional/conditional writes in a durable shared store, not an in-memory map or lock.
Atomically claim an operation across concurrent invocations and worker restarts. Persist intent
@@ -195,7 +194,9 @@ external provider send: it is **not provider-side idempotency or true exactly-on
Entra ID retries or native fallback outside this endpoint can still produce duplicates; confirm that
behavior with the owner rather than promising the local guard prevents it.
-## 5. Integrate one guarded fallback into the handler
+
+
+## 5. Fallback flow
In [SendOtp.js](../javascript/src/functions/SendOtp.js), preserve authentication at the platform
boundary, [envelope validation](../javascript/src/functions/entraPayload.js),
@@ -211,7 +212,7 @@ returns a structured category. Use `providerConfig` for outbound settings and th
[OtpDelivery](../javascript/src/functions/delivery.js) construction and content handling intact.
The existing credential catch immediately returns 502, and transport/result failures immediately
-return errors. Your attempt helper must report the stage to the orchestrator instead of hiding
+return errors. The attempt helper reports the stage to the main flow instead of hiding
it in a catch that calls the secondary. Preserve their final HTTP classifications when fallback
is denied. This is **policy pseudocode**, not executable glue or a complete handler:
@@ -254,9 +255,9 @@ reinterpret generic `Fail` or a broad status range as proof.
[providerTransport.js](../javascript/src/functions/providerTransport.js) collapses fetch and
response-body failures into `provider_timeout`/`provider_network_error`. Those errors do **not**
expose whether the request was transmitted. Treat them as uncertain after intent, including
-connection errors; do not guess they happened before send. A primary credential failure can
-be pre-dispatch only when control flow and operation state prove transport was never entered,
-the failure is isolated to that primary, and account policy still permits the secondary.
+connection errors; do not guess they happened before send. Treat a primary credential failure
+as pre-dispatch only when the flow and operation state prove transport was never entered.
+The failure must also be isolated to the primary, and account policy must still allow the secondary.
Keep endpoint allowlists, HTTPS checks, manual redirects and per-provider response mapping.
Never forward inbound authorization. Preserve the endpoint success nonce/correlation response
@@ -264,22 +265,24 @@ only for evaluation or an accepted delivery result; terminal/uncertain outcomes
nonce. Continue using [safe logging](../javascript/src/functions/logging.js), with an explicit
per-attempt provider ID/classification and one final request outcome.
-## 6. Enforce one aggregate deadline
+
+## 6. Use one end-to-end deadline
-Define an end-to-end budget with the service/caller owner; this guide provides no SLA or magic
-timeout value. Include validation, durable-store waits, primary credential acquisition/transport,
-secondary credential acquisition/transport and final response work. An operation must not receive
-a fresh budget when a duplicate invocation arrives.
+Define one end-to-end time budget with the service and caller owners. Include validation,
+durable-store waits, both providers' credential and transport work, and the final response.
+A duplicate invocation continues to use the original operation budget.
The current credential service has its own acquisition bound and the transport its own timeout.
`parseProviderTimeout` normalizes a single provider timeout, **not** an aggregate deadline.
Add explicit remaining-budget checks and cancellation propagation in your customer integration.
Do not use `Promise.race` to return while a send continues in the background. If ownership,
deadline or cancellation changes after dispatch, record uncertainty and never fall back.
-Reserve enough time for the whole secondary attempt and finalization, rechecking before intent;
-insufficient budget means no secondary even when nonacceptance is otherwise eligible.
+Reserve enough time for the complete secondary attempt and final response, and check the remaining
+time again before sending. Skip the secondary when the remaining time cannot cover that work,
+even when the primary result would otherwise allow fallback.
-## 7. Validate offline, then deploy only after separate approval
+
+## 7. Validation and deployment
Before editing, run the existing JavaScript tests from the repository root. Restore missing
dependencies from the existing lockfile only when needed:
@@ -307,16 +310,13 @@ store faults; block external network. Verify:
- Evaluation returns before operation claims/provider calls; incomplete/decryption failures remain
errors. Credentials, endpoints and wire responses stay isolated between accounts.
-An offline policy-model experiment can check those branches, but a fake classifier does not
-prove any real provider's nonacceptance semantics, and a fake store does not prove distributed
-durability. Routing/isolation tests alone are **not fallback tests**. No production
-nonacceptance classifier is supplied here; keep response-based fallback disabled until reviewed.
+Fake classifiers/stores cannot prove provider nonacceptance or distributed durability.
+Routing/isolation tests alone are **not fallback tests**. No production classifier is supplied;
+keep response-based fallback disabled until reviewed.
-Use one Function App/package and the existing `/api/SendOtp` entry point. Deploy new code for the
-orchestrator, classifier, durable-store integration or code-owned policy changes. Validate
-startup-loaded configuration changes and perform a controlled restart. For secret rotation,
-verify cache refresh; update pinned references explicitly. Repeating an unchanged approved test
-or editing Markdown needs no redeployment.
+Deploy one app/package at `/api/SendOtp` for code-owned policy, orchestrator, classifier, or store changes.
+Validate startup configuration on controlled restart. Verify cache refresh for rotation and update
+pinned references explicitly. Unchanged approved tests or Markdown edits need no redeployment.
Preserve inbound authentication and origin/network restrictions, including any Front Door
forwarding. Regional failover is separate from provider fallback. Before live use, review
diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md
index d63ebd6..32e6c3e 100644
--- a/docs/ONBOARDING.md
+++ b/docs/ONBOARDING.md
@@ -1,60 +1,43 @@
# Customer configuration and validation
-Start with the [five-step setup guide](../README.md). Come here when you need exact IDs,
-provider credential names, or the checks to complete before activation.
+[Documentation](README.md) | [First deployment](GETTING-STARTED.md) | [Troubleshooting](../setup/docs/Troubleshooting.md)
-**After successful guided setup, go straight to [provider authentication](#complete-provider-authentication),
-then [validate the endpoint](#validate-the-deployed-endpoint).** The script has already published
-the code and configured Azure. You do not need local settings, Core Tools, or a second deployment.
+After deployment, complete [provider authentication](#complete-provider-authentication), then
+[validate the endpoint](#validate-the-deployed-endpoint).
-For earlier steps, use [access requirements](#before-purchasing-or-deploying),
-the [values worksheet](#values-and-ownership), or the [setup reference](../setup/docs/README.md).
-[Local development and manual deployment](#optional-local-development-and-manual-deployment)
-are separate, optional paths.
-
-For operational handover, use [Function monitoring](MONITORING.md) and, separately,
-[Microsoft Entra sign-in monitoring](SIGN-IN-MONITORING.md) for authentication outcomes
-and potential abuse investigations. Endpoint setup does not enable tenant log export or alerts.
+| Task | Go to |
+| --- | --- |
+| Confirm access before spending | [Eligibility and approved procedures](#before-purchasing-or-deploying) |
+| Find or check setup values | [Inputs](#inputs-you-supply-to-setup), [setup-managed values](#values-setup-configures-for-you) |
+| Connect the provider | [Credential names and handoff](#complete-provider-authentication) |
+| Test and activate | [Acceptance checks](#validate-the-deployed-endpoint), [operations handover](#activate-operate-and-retire) |
+| Build a custom package | [Custom development and deployment](#custom-development-and-manual-deployment) |
## Before purchasing or deploying
-Open [Microsoft Security Store](https://securitystore.microsoft.com/private-solutions) and review
-the intended provider offer. Confirm account activation, sender registration, supported countries,
-channel, pricing, and access to the provider's **EPP integration**, not just its general messaging API.
-An offer purchase does not deploy Azure resources, enable a Microsoft tenant feature, or install
-a missing adapter.
+In [Microsoft Security Store](https://securitystore.microsoft.com/private-solutions), confirm
+account activation, sender registration, countries, channels, pricing, and EPP access.

-*These are sample cards, not recommendations or a complete provider list. Offers, preview
-labels, pricing, and access can change. A store listing does not mean the setup script supports
-that integration; check [guided provider support](#provider-credential-names).*
+*Sample cards only. Check [guided provider support](#provider-credential-names).*
-Have your tenant administrator confirm the supported EPP onboarding/activation procedure with
-Microsoft and the provider **before incurring deployment costs**. This repository does not define
-tenant eligibility, licensing, preview enrollment, or a self-service activation entitlement.
-The allowed-tenants preview used by setup must also be available in the customer tenant. If an
-offer, preview, authorized test caller, or activation procedure is unavailable, stop at that gate
-and use the offer's available support route or your organization's Microsoft support channel.
-Do not assume that being an Azure subscription owner grants access to these features.
+Before deployment, confirm eligibility, licensing, preview access, an authorized test caller,
+and Microsoft's activation procedure. Use the offer's support route or Microsoft support for
+missing access. Azure subscription ownership does not grant EPP access.
-In these docs, **onboarding owner** means the customer-designated person coordinating the
-tenant/policy administrator, Azure operator, provider administrator, and Microsoft support.
-It is not the name of a contact supplied by this repository. Identify those owners and obtain
-the approved test and policy-change procedures before deployment.
+Assign an **onboarding owner** to coordinate tenant policy, Azure, the provider, and Microsoft support.
-Read the [production limitations](CONTRACT.md#production-limitations): this sample waits for
-provider acceptance, has no durable delivery queue, automatic send retries, deduplication, or
-whole-invocation deadline, and uses one manually renewed decryption key. Caller fallback after
-a timeout can duplicate an already accepted send. Passing setup is not production certification.
+Use the [production-readiness considerations](CONTRACT.md#production-limitations) to plan the
+capabilities your live service needs, such as durable delivery, retry and deduplication strategy,
+an end-to-end deadline, certificate renewal, and delivery confirmation.
## Values and ownership
### Inputs you supply to setup
-Replace placeholders with values from the named portal pages, not values copied from another tenant.
-The [complete noninteractive example](../setup/docs/README.md#noninteractive-example) uses these
-same parameters. No parameter accepts a provider API key or private key.
+Use values from the named portal pages. The
+[noninteractive example](../setup/docs/README.md#noninteractive-example) uses the same parameters.
| Input | Where to find or decide it | Where to set it / required for |
|---|---|---|
@@ -70,38 +53,23 @@ same parameters. No parameter accepts a provider API key or private key.
| Existing certificate location | Azure portal > Key Vault > Certificates > certificate > exact version > Certificate Identifier. Use the `/certificates/` URL, not Show Secret Value. | `-KeyVaultCertificateId`; reuse only. Add `-KeyVaultSubscriptionId` when that vault is in a different subscription in the same customer tenant. |
| Output directory / versions | A private local folder; optionally a reviewed source commit and published release tag. | `-OutputDirectory`, `-SourceRef`, `-PackageReleaseTag`; optional. Keep launcher and support source aligned. |
-**Global/EU is provider-routing metadata, not Azure placement or a data-residency guarantee.**
-The checked-in [Telesign](../setup/providers/telesign.json) and
-[Soprano](../setup/providers/soprano.json) profiles currently use the same URL for Global and EU;
-Soprano also uses the same API app ID/scope. The label alone does not establish where messages,
-credentials, logs, or personal data are processed. Obtain account-level routing and residency
-assurances from the provider and separately choose Azure resource/telemetry locations.
-
-**One guided deployment has one provider and accepts both SMS and voice.** Enabling the second
-channel does not require another setup run; validate and activate each intended channel separately.
-The resource-name suffix uses the subscription ID, app client ID, and prefix, not the provider or
-Azure region. Changing provider/region with the same identifiers can reconfigure the existing
-endpoint rather than add another one. For an independent provider/region deployment, use a distinct
-prefix and a new dedicated app, then validate it before changing policy. Reusing an existing
-app requires an explicit certificate reuse/replacement decision before deployment. A different
-prefix does not authorize key rotation or isolate other endpoints sharing the same app registration.
-The [manual Front Door design](FRONTDOOR.md) requires coordinated same-key regional origins,
-not independent setup runs.
+**Global/EU selects provider routing, not Azure placement or residency.** Current Telesign and
+Soprano profiles use the same Global/EU URLs; Soprano also shares its app ID and scope. Confirm
+provider data locations separately.
+
+**One guided deployment has one provider and both channels.** Validate each channel separately.
+Changing provider or region with the same subscription, app, and prefix reconfigures that
+endpoint. Use a new app and prefix for an independent endpoint. See [Front Door](FRONTDOOR.md)
+for regional origins.
### Values setup configures for you
-Review the deployment plan, then retain `deployment-*.json` and the public certificate in
-`epp-output` (or your chosen directory). Compare the saved `tenantId`, `subscriptionId`,
-`applicationId`, `resources`, `provider`, `endpointRegion`, `providerSmsEndpoint`, and
-`providerVoiceEndpoint` with the approved plan.
-The [setup output guide](../setup/docs/README.md#read-the-deployment-summary) explains the remaining
-fields. `policyChanged: false` is expected, not a failed deployment.
-
-Inspect cloud settings in **Azure portal > Function App > Settings > Environment variables**.
-They are setup-managed; do not recreate them from the local sample or bulk-copy another app's
-settings. Do not export all settings into a ticket. Changes can restart the worker, and reruns
-can restore setup-managed values.
+Keep `deployment-*.json` and the public certificate in private storage. Compare the summary with
+the approved plan. `policyChanged: false` is expected.
+
+Inspect settings in **Function App > Settings > Environment variables**. Keep setup-managed
+values out of tickets and avoid copying settings from another app.
| Value or setting | Source / verification | Customer action |
|---|---|---|
@@ -122,16 +90,13 @@ Guided setup writes both channel-specific endpoint settings and requires a packa
them. Manual configurations must also supply the endpoint for each intended channel;
a missing, empty, or invalid selected endpoint fails closed.
-There are **three separate trust paths**: Easy Auth admits the Microsoft caller; the Key Vault
-certificate decrypts its JWE; provider credentials authorize outbound delivery. The HTTPS
-certificate is separate again. Fix the failing path rather than replacing unrelated IDs or keys.
+The trust paths are separate: Easy Auth checks the caller, the Key Vault certificate decrypts
+the JWE, and provider credentials authorize delivery.
## Complete provider authentication
-Guided setup currently supports **Soprano and Telesign only**. The other bundled adapters are
-developer integrations, not additional guided provider offers. The table is alphabetical,
-not a recommendation order:
+Guided setup supports **Soprano and Telesign**. Other bundled adapters require custom deployment.
| Provider | Credential names / mode | Deployment support |
|---|---|---|
@@ -142,7 +107,7 @@ not a recommendation order:
### Where to enter API-key credentials in Key Vault
-Use the vault created for **your endpoint**, not a vault chosen from a screenshot:
+Use the vault named in the deployment summary:
1. Open the saved deployment summary and find `resources.keyVault`.
2. In **Azure portal > Key vaults**, open that vault.
@@ -151,9 +116,7 @@ Use the vault created for **your endpoint**, not a vault chosen from a screensho

-*This existing test vault contains examples for more than one adapter. Create only the secrets
-required by your chosen integration in the table above. The screenshot does not mean guided
-setup configures multiple providers or supports every adapter.*
+*The screenshot contains test examples. Create only the required secrets.*
4. Choose **Manual**. For **Name**, use the exact name in the provider table above.
For **Secret value**, paste the credential obtained through your provider's secure process.
@@ -163,63 +126,56 @@ setup configures multiple providers or supports every adapter.*

-*The screenshot uses `example-provider-api-key` and a fake masked value to show the form.
-Neither is a working configuration. The form was cancelled without saving. Use the exact
-provider-specific secret names, not this example name, and never reveal a real key for a screenshot.*
-
-After saving, verify the name, enabled state, and version metadata without selecting
-**Show Secret Value**. Do not edit or replace the setup-created encryption certificate secret.
-If the vault denies access, ask the approved credential administrator for help; do not disable
-vault protection or copy credentials into Function settings. See the permission and runtime
-checks in the provider-specific steps below.
-
-### Telesign: enter and verify the two vault secrets
-
-1. Obtain the raw API key and matching Customer ID from your Telesign account's approved secure
- credential process. Confirm that this account can call the configured EPP URL for each channel you intend to enable.
-2. Open the **exact Key Vault in `resources.keyVault`** in the deployment summary. In
- **Objects > Secrets > Generate/Import**, choose manual entry. Create `telesign-api-key` with
- the raw API key, then `telesign-customer-id` with the matching raw Customer ID.
- Do not base64-encode the pair, decode the key, or enter an Authorization header; the adapter
- constructs Basic authentication. Keep both secrets enabled with validity dates appropriate
- to your provider agreement.
-3. Verify names, enabled state, and version metadata without selecting **Show Secret Value**.
- In **Access control (IAM)** confirm the Function's **system-assigned identity** has Key Vault
- Secrets User at this vault. Setup already assigns this and gives its Azure operator Key Vault
- Secrets Officer. A different credential administrator needs separately approved vault access;
- Entra administrator status alone does not grant vault data access.
-4. Allow RBAC propagation and check vault network access if writes/reads fail. Do not replace
- the encryption certificate secret, grant broad access, or put credentials into Function
- settings to bypass a vault failure.
-5. Follow the authorized evaluation and controlled live checks below. Evaluation cannot validate
- these secrets. Credential caching depends on your settings: setup disables it for FC1 and
- enables it for EP1. Use the [runtime cache behavior](CONTRACT.md#credential-caching-and-refresh)
- to plan first-use and rotation checks. Absence of a warning does not prove account authorization.
+*Use the exact provider secret name. The shown value is fake.*
+
+After saving, verify the name, enabled state, and version without viewing the value. Keep the
+setup-created encryption certificate unchanged.
+
+
+### Telesign credentials
+
+1. Confirm account access to each intended channel's EPP URL. Use the
+ [vault steps above](#where-to-enter-api-key-credentials-in-key-vault) to store the raw API key as
+ `telesign-api-key` and matching raw Customer ID as `telesign-customer-id`.
+ Do not encode the pair, decode the key, or enter an Authorization header; the adapter builds Basic auth.
+2. Verify enabled state, validity, and version metadata without **Show Secret Value**.
+ In **Access control (IAM)**, confirm the Function's **system-assigned identity** has
+ **Key Vault Secrets User**. Setup gives its Azure operator **Key Vault Secrets Officer**;
+ another credential administrator needs separate vault access, regardless of directory role.
+ For failures, allow RBAC propagation and check networking; never replace the encryption secret,
+ grant broad access, or copy credentials into app settings to bypass a denial.
+3. Complete the [authorized evaluation and live checks](#validate-the-deployed-endpoint).
+ Evaluation does not validate these secrets. Setup disables provider caching for FC1 and enables
+ it for EP1; plan first-use/rotation checks from the [cache contract](CONTRACT.md#credential-caching-and-refresh).
+ No warning is not proof of account authorization.
Use portal secret entry rather than command-line literal values, transcripts, source files,
screenshots, or chat. During rotation, coordinate the matching pair and provider validity window;
two secret updates are not atomic. Keep old credentials usable during the approved transition
where the provider supports it, and verify live delivery before retiring them.
-### Soprano: provider-administrator handoff
+
+### Soprano credentials
+
+Setup creates customer-side federation, **not** the commercial account or access in Soprano's
+tenant. Complete this handoff before treating OAuth as ready.
+
+Send **identifiers only** through the provider's approved onboarding channel:
-Setup configures the customer-side multitenant app and a federated identity credential trusting
-the outbound managed identity. It does **not** provision your commercial account or grant
-application access in Soprano's tenant. A successful Azure deployment is therefore not evidence
-that Soprano OAuth is ready.
+- customer tenant ID and `applicationId` / `EPP_OUTBOUND_CLIENT_ID`
+- intended channels and their full provider URLs
+- `EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_APP_ID`, and `EPP_PROVIDER_SCOPE`
+- the provider-requested account or sender reference
-Send the following **identifiers only** through the provider's approved onboarding channel:
-customer tenant ID; `applicationId` / `EPP_OUTBOUND_CLIENT_ID`; intended channels and their full provider URLs;
-`EPP_PROVIDER_TENANT_ID`, `EPP_PROVIDER_APP_ID`, and `EPP_PROVIDER_SCOPE`; and the account/sender
-reference requested by the provider. If troubleshooting federation, also supply the outbound
-identity Client ID and Principal ID, and the federated credential's issuer/subject/audience
-metadata. Never supply the managed-identity assertion, access token, private key, or a new client secret.
+For federation troubleshooting, also provide the outbound identity Client ID and Principal ID,
+plus the federated credential's issuer, subject, and audience. Keep the managed-identity assertion,
+access token, private key, and client secrets private.
-Ask the provider administrator to confirm the API tenant/app/scope, provision/authorize the
-calling application's service principal in that tenant through its supported process, and
-confirm the exact consent/application roles and account/channel entitlement. This repository
-does not publish a supported Soprano consent URL or role-ID catalog; do not guess a role or
-construct an admin-consent link from the sample's GUIDs. Record their completion confirmation.
+Ask the provider administrator to confirm the API tenant, app, and scope. They can then authorize
+the calling application's service principal through the provider's supported process and confirm
+the exact consent, application roles, and account/channel access. Use the provider's approved
+consent path rather than building a URL or role list from sample GUIDs. Record the completion
+confirmation.
Inspect the dedicated app's **Certificates & secrets > Federated credentials** and the outbound
identity's Overview. Setup's subject is the identity's **Principal ID**, issuer is the customer's
@@ -231,17 +187,17 @@ for the mechanism, then use an authorized controlled live test to verify the ful
## Validate the deployed endpoint
-**Authorized testing is an assisted gate.** This repository supplies offline runtime tests, not
-a customer self-service tool that obtains a token as Microsoft's phone-provider application.
-An ordinary `az account get-access-token` result or a customer's test app is not that caller.
-Do not add a test application to `allowedApplications`, disable Easy Auth, or publish a local
-unauthenticated host to work around this. Ask the onboarding owner to arrange Microsoft's
-approved EPP caller/test procedure. If unavailable, record **not validated** and do not activate.
-
-Provide that operator the endpoint URL, customer tenant ID, endpoint application client ID,
-identifier URI, encryption key ID, **public** certificate/thumbprint, and provider from your
-inventory/summary, plus the intended channels and approved test window. Agree on controlled
-recipients privately and obtain explicit permission before any paid/live send.
+**Use Microsoft's assisted process for authorized testing.** The repository provides offline
+runtime tests, while Microsoft supplies the approved EPP caller and test procedure. An ordinary
+`az account get-access-token` result or a customer test app represents a different caller.
+Keep `allowedApplications` and Easy Auth unchanged, and keep local hosts private. Ask the
+onboarding owner to arrange the approved procedure. Activate after that validation is complete.
+If the procedure is not available, record **not validated** and leave the policy inactive.
+
+Provide the operator with the endpoint URL, customer tenant ID, endpoint application client ID,
+identifier URI, encryption key ID, and **public** certificate or thumbprint. Also provide the
+selected provider, intended channels, and approved test window from the deployment inventory.
+Agree on controlled recipients privately and get permission before any paid live send.
| Order / owner | Check | Evidence to retain without sensitive payloads |
|---|---|---|
@@ -277,25 +233,22 @@ runs and repeats the message. Verify the actual locale, spoken digits, and recip
Do not blindly retry timeouts: provider acceptance can precede a lost response or caller fallback.
Repeat deployed checks after package, authentication, credential, key, or routing changes.
-## Activate, operate, and retire
+
+## Operations lifecycle
Only after the acceptance checks pass, have the policy administrator follow
[manual activation and rollback](../setup/docs/README.md#step-3---manually-validate-and-activate-policy).
Public Graph SMS/voice schemas do not document the EPP `url`/`appId` update. Obtain the
product-approved procedure; no guessed PATCH body is supplied here.
-Open **Function App > Application Insights** to locate the associated component, then its linked
-**Log Analytics workspace > Logs** for the workspace-schema queries in
-[MONITORING.md](MONITORING.md). Start with request discovery, then inspect the runtime-specific
-events; do not search every language for `logType: "request"`. Current implementations use
-`request_completed` service/log events, not a universal JSON summary.
+For handover, [find the linked monitoring resources](MONITORING.md#find-the-linked-resources-in-the-portal),
+discover requests, then use [your runtime's event schema](MONITORING.md#runtime-specific-log-discovery).
+There is no universal JSON request summary; operation, invocation, and application IDs differ.
+Easy Auth can reject before application logs exist; `response_prepared` does not prove caller receipt.
-Use the [Application Insights guide](APPLICATION-INSIGHTS.md) for collection/identity/sampling,
-[runtime log queries](MONITORING.md#runtime-specific-log-discovery) for Python/.NET as well as
-JavaScript, and [troubleshooting](../setup/docs/Troubleshooting.md#after-deployment-triage) when
-data or delivery is missing. Correlate the host operation/invocation and application request IDs
-without treating them as interchangeable. Easy Auth can reject a call before application logs
-exist. A `response_prepared` event does not prove the caller received the response.
+Use [collection diagnostics](APPLICATION-INSIGHTS.md) for missing data and
+[endpoint triage](../setup/docs/Troubleshooting.md#after-deployment-triage) for failures.
+[Identity monitoring](SIGN-IN-MONITORING.md) is separate; setup does not export tenant logs or create alerts.
Assign ongoing owners for request failures/latency, provider account and credential rotation,
certificate expiry/renewal, alert routing, telemetry gaps, retention/cost, and policy rollback.
@@ -304,14 +257,14 @@ certificate contacts, or provider delivery receipts. Complete those manual opera
relying on alerts. Keep the saved summary, approved policy snapshots, validation evidence,
package/source version, and renewal dates in restricted operational storage.
-## Optional local development and manual deployment
+
+## Custom development and manual deployment
-**Skip this section after guided setup unless developing a custom package.** Follow the selected
+**Skip after guided setup unless customizing code.** Follow the
[JavaScript](../javascript/README.md), [Python](../python/README.md), or [.NET](../dotnet/README.md)
-guide. Copy [local.settings.sample.json](local.settings.sample.json) to `local.settings.json`
-beside that runtime's `host.json`, replace placeholders, and keep every `Values` entry a string.
-Use `node`, `python`, or `dotnet-isolated` for `FUNCTIONS_WORKER_RUNTIME`.
+local setup. Copy [sample settings](local.settings.sample.json) beside that runtime's `host.json`;
+replace placeholders, use string `Values`, and select `node`, `python`, or `dotnet-isolated`.
`UseDevelopmentStorage=true` needs local Azurite. Use a local **test** PEM/base64 PEM for
`EPP_DECRYPTION_KEY_PEM`; Core Tools cannot resolve Azure Key Vault references. For evaluation-only
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..d6283b8
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,60 @@
+# Documentation by task
+
+[Sample overview and architecture](../README.md)
+
+**First deployment:** follow [Get started](GETTING-STARTED.md). Confirm access and Microsoft's
+approved test and activation process before deploying in a nonproduction environment.
+
+## Table of Contents
+
+- [Understand the sample](#understand-the-sample)
+- [Deploy and validate](#deploy-and-validate)
+- [Operate](#operate)
+- [Develop](#develop)
+- [Advanced topics](#advanced-topics)
+- [Get help](#get-help)
+
+## Understand the sample
+
+- [Architecture and implementation](../TECHNICAL.md)
+- [HTTP contract](CONTRACT.md)
+- [Log schema](LOGGING.md)
+- [Production readiness](CONTRACT.md#production-limitations)
+
+## Deploy and validate
+
+| Task | Guide |
+| --- | --- |
+| Register an app, deploy, test, and hand over | [Five-step setup](GETTING-STARTED.md) |
+| Find IDs and complete provider authentication | [Configuration and validation](ONBOARDING.md) |
+| Check permissions, script choices, or deployment output | [Setup reference](../setup/docs/README.md) |
+| Resolve a setup or endpoint failure | [Troubleshooting](../setup/docs/Troubleshooting.md) |
+| Estimate SMS/voice provider charges | [Cost estimate](COST-CALCULATOR.md); not a billing report |
+
+## Operate
+
+- [Function logs and alerts](MONITORING.md)
+- [Application Insights collection](APPLICATION-INSIGHTS.md)
+- [Microsoft Entra ID monitoring](SIGN-IN-MONITORING.md)
+- [Certificate renewal](../setup/docs/README.md#encryption-certificate-lifecycle)
+- [Rollback and decommissioning](../setup/docs/README.md#rollback-and-decommissioning)
+
+## Develop
+
+- Runtimes: [JavaScript](../javascript/README.md), [C#](../dotnet/README.md), [Python](../python/README.md)
+- [Settings and packages](../TECHNICAL.md)
+- [HTTP contract](CONTRACT.md)
+- [Log schema](LOGGING.md)
+- [Phone-service integration](BRING-YOUR-OWN-PROVIDER.md)
+
+
+## Advanced topics
+
+- [Multi-region routing](FRONTDOOR.md)
+- [Provider fallback](MULTI-PROVIDER-IMPLEMENTATION.md)
+
+## Get help
+
+- [Provider access, testing, and activation](ONBOARDING.md#before-purchasing-or-deploying)
+- [Troubleshooting](../setup/docs/Troubleshooting.md)
+- [Safe diagnostic evidence](../setup/docs/Troubleshooting.md#after-deployment-triage)
diff --git a/docs/SIGN-IN-MONITORING.md b/docs/SIGN-IN-MONITORING.md
index 31fb9bb..ed42887 100644
--- a/docs/SIGN-IN-MONITORING.md
+++ b/docs/SIGN-IN-MONITORING.md
@@ -1,72 +1,58 @@
# Monitor Microsoft Entra sign-ins
-For ongoing monitoring, start with the [three recommendations below](#recommended-monitors).
-To look up one failed sign-in, go to [Step 1](#1-choose-your-tenant-and-access).
-The six-step walkthrough creates a user sign-in alert; the [optional reference](#optional-reference)
-helps your query maintainer adapt it to the other monitors.
+[Documentation](README.md) | [Function monitoring](MONITORING.md)
-**Scope: Entra authentication and configuration, not Function or phone delivery.**
-Keep identity details private. EPP setup does not export these logs or create alerts;
-the steps below are actions for your administrators, not changes made by this guide.
+| Task | Start here |
+| --- | --- |
+| Investigate one sign-in without creating resources | [Choose tenant/access](#1-choose-your-tenant-and-access), then [find the sign-in](#2-find-one-sign-in) |
+| Set up ongoing identity alerts | [Recommended monitors](#recommended-monitors), then [export](#3-export-logs-for-ongoing-monitoring) and follow Steps 4-6 |
+| Diagnose missing records or quiet alerts | [Check data](#4-check-your-data-and-choose-a-query), [monitor ingestion](#monitor-ingestion-separately) |
+| Adapt a query or add advanced alerts | [Advanced monitoring reference](#advanced-monitoring-reference) |
+
+The six-step walkthrough creates one user sign-in alert. Function and delivery monitoring are separate.
## Recommended monitors
-Put these in place in priority order. First agree on the source tenant, exact integration
-identities and target resources, and who receives each notification. Use IDs, not name
-substrings or every application in the tenant.
+Choose the tenant, exact identity/resource IDs, and notification owners. Use exact IDs.
-### Priority 1: Integration identities cannot sign in
+
+### Integration sign-in failures
1. **Watch:** application/service-principal and managed-identity authentication in
**AADServicePrincipalSignInLogs** and **AADManagedIdentitySignInLogs**. Filter to the
known app/service principal **and** intended resource in the source tenant.
-2. **Notify:** when failures repeat above the usual level for the same observation window,
- or a new error starts recurring. Group by identity, resource, and error; show the last
- successful sign-in alongside failures. Start with a review notification, then agree
- with the owner which sustained failures need paging.
-3. **Owner and first checks:** the Azure/identity integration operator checks the error,
- identity status, credential validity, federation settings, and recent permission changes.
- Token success does not prove a later API operation is authorized. Token caching means
- there is not a sign-in event for every service request: only alert on absent successes
- after accounting for the expected schedule, caching, and [collection health](#monitor-ingestion-separately).
-
-### Priority 2: Integration credentials, permissions, or trust change
+2. **Notify:** when failures exceed the normal level or a new error recurs. Group by identity,
+ resource, and error; include the last successful sign-in.
+3. **First checks:** review the error, identity, credentials, federation, permissions, expected
+ schedule, token caching, and [collection health](#monitor-ingestion-separately).
+
+
+### Credential and permission changes
1. **Watch:** **AuditLogs** for the integration's application and service-principal objects.
Include credential additions/removals, application identifier or trust-setting updates,
service-principal changes, consent, and permission grants/removals. Have the maintainer
verify which [audit activities](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/reference-audit-activities#core-directory)
actually appear for these operations.
-2. **Notify:** on an unexpected **successful** change to those targets; one can deserve
- review without a volume threshold. Keep failed attempts separate from completed changes.
- Record approved maintenance so planned changes do not page as suspected abuse.
-3. **Owner and first checks:** the application/identity administrator checks who changed
- what, when, and against which approved change ticket. Review target and changed-property
- names; link to restricted logs instead of attaching raw old/new values. Group related
- records before escalating an unexplained change to security. This is change monitoring,
- not a credential-expiry inventory or automatic revocation rule.
+2. **Notify:** on an unexpected **successful** change. Keep failed attempts separate and record
+ approved maintenance.
+3. **First checks:** review the actor, target, time, approved change, and changed-property names.
+ Link to restricted logs instead of copying raw values.
-### Priority 3: Users repeatedly have trouble signing in
+
+### Repeated user sign-in failures
1. **Watch:** **SigninLogs** for the customer-facing app and source tenant. Review
**AADNonInteractiveUserSignInLogs** separately for background token/session problems;
do not mix those records into the interactive failure count or rate.
-2. **Notify:** when the same error keeps recurring or unresolved failures rise above
- ordinary traffic for the agreed window. Start with counts; use a rate only with a
- matching population and minimum traffic level. A consent prompt or expired session
- can recover, so check later outcomes before treating it as a continuing problem.
-3. **Owner and first checks:** identity support reads the overall error and reason,
- then checks recent app/policy changes and related recovery records. Successful
- Conditional Access or MFA steps do not make the overall sign-in successful.
- Escalate to security only with corroborating signals, not a nonzero code alone.
-
-**Set up once, then adapt.** Follow [export setup](#3-export-logs-for-ongoing-monitoring),
-select the [categories for these monitors](#choose-additional-export-categories), and use
-the [field checks](#field-checks-for-the-recommended-monitors) before adapting a query.
-Reuse the [alert settings](#5-create-your-first-alert) and [safe test/handover](#6-test-then-hand-over);
-the walkthrough's user-failure query and measurement are not interchangeable with the
-other monitors. Choose thresholds from representative ordinary traffic, not a small export
-or a fixed production SLA.
+2. **Notify:** when an error recurs or unresolved failures exceed the baseline. Check later
+ outcomes because consent prompts and expired sessions can recover.
+3. **First checks:** review the overall error, recent app or policy changes, and recovery records.
+ Successful MFA steps do not make the overall sign-in successful.
+
+Select the [required categories](#choose-additional-export-categories), verify
+[fields](#field-checks-for-the-recommended-monitors), and reuse the
+[alert setup](#5-create-your-first-alert). Set thresholds from normal traffic.
## 1. Choose your tenant and access
@@ -75,16 +61,15 @@ or a fixed production SLA.
from the tenant hosting the Function. Note its **Tenant ID** from **Overview**.
2. Ask your tenant administrator for **Reports Reader**, the minimum role for viewing
activity logs. See Microsoft's [access requirements](https://learn.microsoft.com/en-us/entra/identity/monitoring-health/howto-access-activity-logs#prerequisites).
- This role does not grant access to an Azure workspace (where exported logs are stored).
+ This role does not grant workspace access.
3. Check whether you have a **workforce tenant** or an **external tenant** for customer apps.
- If unsure, use the [tenant comparison](https://learn.microsoft.com/en-us/entra/external-id/customers/concept-supported-features-customers)
- with your administrator. Both can use the portal investigation below, but external
- tenants need a different export setup in Step 3.
+ Use the [tenant comparison](https://learn.microsoft.com/en-us/entra/external-id/customers/concept-supported-features-customers)
+ if needed. External tenants use a different export path in Step 3.
## 2. Find one sign-in
1. Open **Entra ID > Monitoring & health > Sign-in logs**. Choose a date range and filter
- to the application you're investigating. Open a record you're authorized to inspect.
+ to the application under investigation. Open a record authorized for inspection.
2. In **Basic info**, check the **Application**, **Application ID**, **Resource**, outcome,
and time. Keep the application's **client ID**, not its Object ID, for later query filters.
This may not be the endpoint `applicationId` in your EPP deployment summary.
@@ -109,9 +94,8 @@ or a fixed production SLA.
If the issue points to the endpoint or provider, follow the
[Function monitoring guide](MONITORING.md#runtime-specific-log-discovery) separately.
-**Only investigating one problem? You can stop here.** You don't need to export logs, create
-a workspace, or incur additional Azure Monitor collection costs for this portal-only path.
-Continue if your team wants ongoing alerts or longer-term analysis.
+A single investigation can end after the portal review. Ongoing alerts, longer-term analysis,
+and shared monitoring require the remaining steps.
## 3. Export logs for ongoing monitoring
@@ -176,12 +160,12 @@ Continue if your team wants ongoing alerts or longer-term analysis.
for that window, not a whole week's total. There is no
universal threshold; the source query is not a turnkey EPP or fraud detector.
5. Select **Save > Save as query** and choose the agreed query pack. Saving needs
- **Log Analytics Contributor**; ask an authorized teammate if you only have read access.
+ **Log Analytics Contributor**; an authorized operator must make changes when access is read-only.
See [saving queries](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/save-query).
- Keep the filters, count units, baseline, and chosen threshold with your team's setup notes.
+ Keep the filters, count units, baseline, and chosen threshold with the monitoring setup notes.
-You now have one scoped query to reuse for the first alert. Leave rate, volume, and
-IP-based detection for [later](#more-alert-types), once you understand this baseline.
+The scoped query can be reused for the first alert. Add rate, volume, and IP-based detection
+only after establishing the [baseline](#more-alert-types).
## 5. Create your first alert
@@ -197,13 +181,13 @@ IP-based detection for [later](#more-alert-types), once you understand this base
table plan supports the query and rule.
3. For **Failed Signin reasons**, set **Measure** to **Calculation of a numeric column**,
choose `Count`, and use **Total** across the result-code groups.
- Leave **Split by dimensions** unset for this first combined alert. Don't count summary
+ Leave **Split by dimensions** unset for this first combined alert. Do not count summary
rows as failures. Set **Static**, **Greater than**, and a **Threshold value** based on
the normal counts for the same observation window in Step 4.
4. Set **Aggregation granularity (window size)** and **Frequency of evaluation** to match
the team's observation window, ingestion delay, response needs, and budget. Review
**Override query time range** with the query's time filter; alert scans are limited to
- **48 hours**. Start with one evaluation above the threshold; if you need several, review the
+ **48 hours**. Start with one evaluation above the threshold; multiple evaluations require review of the
[required query output and evaluation settings](#query-identity-and-rule-options) first.
Review healthy and qualifying periods in Preview. Keep missing-data expectations separate from failure counts;
[collection-health guidance](#monitor-ingestion-separately) explains why.
@@ -214,22 +198,23 @@ IP-based detection for [later](#more-alert-types), once you understand this base
6. Choose the query **Identity**. For a **system-assigned managed identity**, clear
**Enable upon creation**, create the rule disabled, and have your Azure administrator
grant it reader/query access to the workspace. Wait for role propagation before enabling.
- An interactive query working under your account doesn't prove the rule can read logs.
+ An interactive query working under the operator account does not prove the rule can read logs.
Review **Automatically resolve alerts** and [identity options](#query-identity-and-rule-options)
before **Review + create**. Keep the rule disabled until ready for the test below.
-## 6. Test, then hand over
+
+## 6. Test and hand over
1. Save the action group, open it, and select **Test**. Choose the notification receivers
and wait for the result; also check that the team receives the message. Microsoft's
[testing instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups#test-an-action-group-in-the-azure-portal)
cover failures and test permissions. This checks routing, not the sign-in query.
-2. Test the query and a **temporary nonproduction rule** against existing records you're
- allowed to inspect. Use a restricted test group and a temporary condition those records
+2. Test the query and a **temporary nonproduction rule** against authorized existing records.
+ Use a restricted test group and a temporary condition those records
satisfy. Check the query's window and portal override together. Verify the scheduled
alert and its later resolution, then remove the temporary rule/settings. See the
[full safe-test procedure](#validate-without-generating-provider-sends).
-3. Don't generate live OTPs, repeated wrong codes, or production policy changes to populate
+3. Do not generate live OTPs, repeated wrong codes, or production policy changes to populate
a chart. If records are insufficient, use an approved **non-phone test sign-in** in a
test tenant/app. An EPP evaluation does not guarantee a sign-in record or prove delivery.
4. Before enabling ongoing notifications, check that logs arrive, the rule identity has
@@ -237,39 +222,39 @@ IP-based detection for [later](#more-alert-types), once you understand this base
Leave a teammate the query, rule, workspace, action group, settings, and support contacts.
Arrange a check for [missing logs](#monitor-ingestion-separately), not just sign-in failures.
-Your first alert is ready once these checks pass. Use the optional reference below when
-you need more advanced monitoring.
+Enable the ongoing alert only after these checks pass. Advanced options follow below.
-## Optional reference
+
+## Advanced monitoring reference
-Use these sections when you need more than the first alert: [tenant setup and access](#1-choose-the-tenant-and-permissions),
-[monitor field checks](#field-checks-for-the-recommended-monitors), [query details](#4-establish-the-baseline),
-[other alert types](#more-alert-types),
-[collection gaps](#monitor-ingestion-separately), [Workbooks](#6-build-a-sign-in-workbook),
-and [triage, retention, and costs](#7-triage-privacy-retention-and-costs).
+For query maintainers: [access/licensing](#tenant-setup-access-and-licensing),
+[external tenants](#prepare-the-external-tenant-access-path), [categories](#choose-additional-export-categories),
+[schema](#inspect-schema-and-arrival-delays), [field checks](#field-checks-for-the-recommended-monitors),
+[query adaptation](#adapt-official-queries), [rule options](#query-identity-and-rule-options),
+[other alerts](#more-alert-types), [safe testing](#validate-without-generating-provider-sends),
+[Workbooks](#build-a-workbook-when-you-need-a-shared-view), and [triage/costs](#investigate-alerts-and-manage-the-data).
### Understand the evidence
-1. Counts represent **stored sign-in records**, not unique customers, completed sessions,
- or sends. Several records can describe one authentication sequence. A successful sign-in
- can use an existing token claim without a phone challenge.
-2. Invalid passwords, interrupted challenges, cancellation, and policy rejection can all
- cause non-success results. Neither an SMS/Voice label nor an MFA requirement identifies
- the provider. `SigninLogs` has no guaranteed EPP delivery-mode, provider-attribution,
- phone-delivery, or provider-cost field.
-3. Use [Function request queries](MONITORING.md#2-single-region-function-queries) and
- [Application Insights](APPLICATION-INSIGHTS.md) for endpoint execution. Use the provider's
- delivery and billing reports for recipient delivery and charges. Evaluation and live
- Function requests can both return HTTP 200; neither proves handset receipt.
+Counts are **stored records**, not unique customers, sessions, or sends. One authentication can
+produce several records or succeed from an existing token claim without a phone challenge.
+Non-success can mean an invalid password, interruption, cancellation, or policy rejection.
+SMS/Voice/MFA labels do not identify the provider; `SigninLogs` guarantees no EPP mode,
+provider-attribution, delivery, or cost field.
+
+Use [Function queries](MONITORING.md#2-single-region-function-queries) for execution, and provider
+reports for delivery/billing. Evaluation and live requests can both return HTTP 200; neither proves receipt.
-### Tenant setup, access, and licensing
+
+### Tenant access and licensing
-1. Entra roles and Azure RBAC are separate. **Reports Reader** lets you view portal activity;
- **Security Administrator** is needed for general diagnostic settings. Review
+1. Microsoft Entra ID roles and Azure RBAC are separate. Use the roles in
+ [portal access](#1-choose-your-tenant-and-access) and [export setup](#3-export-logs-for-ongoing-monitoring).
+ Review
[workspace/table access](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/manage-access)
separately for queries. An external-tenant delegation does not grant every alert-management
or role-assignment operation in the subscription-owning workforce tenant.
@@ -279,7 +264,7 @@ and [triage, retention, and costs](#7-triage-privacy-retention-and-costs).
3. The [external-tenant feature comparison](https://learn.microsoft.com/en-us/entra/external-id/customers/concept-supported-features-customers#general-feature-comparison)
currently lists **ID Protection as unavailable in external tenants**. Full supported
workforce [ID Protection capabilities](https://learn.microsoft.com/en-us/entra/id-protection/overview-identity-protection#license-requirements)
- require P2 or an applicable entitlement. Don't buy workforce P2 expecting it to enable
+ require P2 or an applicable entitlement. Do not buy workforce P2 expecting it to enable
external-tenant risk detections.
### Prepare the external-tenant access path
@@ -296,7 +281,7 @@ and [triage, retention, and costs](#7-triage-privacy-retention-and-costs).
name and region. In **Select user access**, choose the agreed users/groups and Azure roles.
3. Review the delegation, destination, region, permissions, and costs before **Create**.
Wait for confirmation, select **Done**, then configure export as in Step 3.
- Don't move the EPP subscription or alter authentication policy to imitate workforce setup.
+ Do not move the EPP subscription or alter authentication policy to imitate workforce setup.
@@ -322,22 +307,19 @@ and [triage, retention, and costs](#7-triage-privacy-retention-and-costs).
### Inspect schema and arrival delays
-1. Check your workspace [query scope](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#open-log-analytics),
- not the tutorial's demo environment or Application Insights `requests`/`traces` context.
- Use [Simple mode's table selector](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-simple-mode#get-started-in-simple-mode)
- if you prefer a point-and-click preview.
-2. Expand a row using its [chevron](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#expand-and-sort-records).
- `AADTenantId` is the source directory; standard `TenantId` is the **workspace ID**.
- `AppId` is the app client ID, not an Object ID or provider API ID.
-3. In **SigninLogs**, `ResultType` is a **string**: zero indicates success; populated
- nonzero values are authentication codes. Keep empty values as unknown.
- `CreatedDateTime` is sign-in initiation in UTC; `TimeGenerated` is the workspace log
- timestamp. Use the ingestion-time guidance below rather than assuming their difference
- measures collection delay.
-4. Adapt the [ingestion-latency reference](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-ingestion-time)
+1. Check the [workspace scope](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#open-log-analytics),
+ not a demo workspace or Application Insights `requests`/`traces` context.
+ Preview with [Simple mode](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-simple-mode#get-started-in-simple-mode)
+ and [expand a row](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#expand-and-sort-records).
+2. Verify IDs and result types with the [field checks below](#field-checks-for-the-recommended-monitors).
+ In `SigninLogs`, `ResultType` is a **string**: zero is success, populated nonzero values are
+ authentication codes, and empty values remain unknown.
+3. `CreatedDateTime` is sign-in initiation in UTC; `TimeGenerated` is the workspace timestamp,
+ not an ingestion-delay measurement. Adapt the
+ [ingestion-latency reference](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-ingestion-time)
and [standard-column timing guidance](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-standard-columns#_timereceived-column)
to your `SigninLogs` scope to measure delay, including missing timestamps.
- These generic examples are not a ready-made Entra monitor or delivery-latency measure.
+ These are not ready-made identity monitors or delivery-latency measures.
### Field checks for the recommended monitors
@@ -389,10 +371,10 @@ saving an alert query. Portal JSON downloads are not Log Analytics rows.
previous authentication can satisfy a step; distinguish the step outcome from the whole sign-in.
2. Microsoft's [MFA Workbook template](https://github.com/microsoft/external-id-azure-monitor/blob/main/templates/mfaWorkbookDeployment.json)
currently queries **AuditLogs**, not `SigninLogs` authentication-step arrays. It needs
- corresponding audit export/activity. Don't substitute tables or read it as a full step sequence.
+ corresponding audit export/activity. Do not substitute tables or read it as a full step sequence.
3. A `SigninLogs` step report must handle `AuthenticationDetails` as the documented string
containing parsed array data, with empty/invalid/missing data and nullable outcomes.
- Array expansion counts **steps**, not sign-ins. Missing steps aren't success or failure.
+ Array expansion counts **steps**, not sign-ins. Missing steps are not success or failure.
Keep `authenticationMethodDetail` and raw payloads out of shared views; they can contain
phone identifiers and other personal data.
@@ -402,17 +384,17 @@ saving an alert query. Portal JSON downloads are not Log Analytics rows.
1. In the [SigninLogs query collection](https://learn.microsoft.com/en-us/azure/azure-monitor/reference/queries/signinlogs),
**Failed Signin reasons** groups by code/reason; **Failed login Count** counts failures by
- resource; **Signin Locations** groups success/failure by location. Pick the measure you need.
+ resource; **Signin Locations** groups success/failure by location. Select the required measure.
Some examples contain personal fields or limit MFA failures to one code; none is a
complete endpoint/provider-failure detector.
2. Add your source tenant/app filters **before aggregation**. Use the
[filtering tutorial](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#apply-multiple-query-filters)
or [Simple mode column filters](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-simple-mode#filter-by-column),
- then switch to KQL to check them. Results-grid searches/header filters don't persist
+ then switch to KQL to check them. Results-grid searches/header filters do not persist
as alert-query filters. Review numeric-looking code comparisons against the string schema.
3. Use the [time-range controls](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/log-analytics-tutorial#set-the-query-time-range)
with any query time filter and verify the effective range. Preview **Show** limits and
- browser-side grouping can hide data; don't mistake them for the full baseline.
+ browser-side grouping can hide data; do not treat them as the full baseline.
4. Save the reviewed query and share its location with a teammate who has log access.
Check that they get the same filters and columns. Distinct-user, record-count, and step
measures differ; note unknown-code handling and any exclusions alongside the query.
@@ -441,7 +423,7 @@ saving an alert query. Portal JSON downloads are not Log Analytics rows.
5. In a new action group, complete **Basics**, **Notifications**, and any **Actions**, then
**Review + create > Create**. Return to the rule and select the saved group.
Check processing region, recipients, required verification, and **Common alert schema**
- where applicable. Keep security results restricted; don't attach automatic policy changes
+ where applicable. Keep security results restricted; do not attach automatic policy changes
to a heuristic.
6. **Automatically resolve alerts** makes the rule stateful; resolution timing depends on
frequency. Review **Mute actions** with the receiving team. Edit an existing rule through
@@ -479,13 +461,13 @@ reviewed query, baseline, and response plan.
choosing a grouped rule. Account for unknown users, absent IPs, approximate distinct
counts, distributed attempts, NAT, VPNs, mobile networks, and shared proxies.
IP geolocation does not prove physical location.
-3. Restrict personal-data results/notifications. Don't automatically block, disable an
+3. Restrict personal-data results/notifications. Do not automatically block, disable an
account/provider, or change policy solely on these heuristics.
4. In supported, licensed workforce tenants, follow
[risk investigation instructions](https://learn.microsoft.com/en-us/entra/id-protection/howto-identity-protection-investigate-risk).
Check populated `RiskLevelDuringSignIn`, `RiskLevelAggregated`, `RiskState`, and `RiskDetail`.
- Detections can arrive asynchronously; empty, none, or hidden isn't fraud clearance.
- A template named "risk detection" doesn't enable ID Protection in external tenants.
+ Detections can arrive asynchronously; empty, none, or hidden is not fraud clearance.
+ A template named "risk detection" does not enable ID Protection in external tenants.
### Monitor ingestion separately
@@ -498,80 +480,51 @@ reviewed query, baseline, and response plan.
arrival**; test each case. It is not a preconfigured no-data alert. An unresolved table
or query error must remain visible, not become an empty-success fallback.
3. Create a separate rule with its own measure, threshold, lookback, frequency, recipient,
- and maintainer. Match the query-time override to that calculation; don't blindly use
+ and maintainer. Match the query-time override to that calculation; do not blindly use
the aggregation window. Check the query's frequency restrictions.
4. Inspect rule health/query errors, export settings, workspace access, transformations,
ingestion limits, and service health. Fix evaluation failures separately from stale-data
- incidents. Missing evidence doesn't mean less abuse.
+ incidents. Missing evidence does not mean less abuse.
### Validate without generating provider sends
-1. Save the action group before **Test**. Keep the test page open until **Success** or
- **Failed**; on failure use **View details**, fix, save, and retest. Check recipient
- verification and spam filtering. Group read access alone doesn't grant test permission.
-2. Compare the saved query with existing portal records in healthy, qualifying, empty,
- and delayed windows. Check output units against Measurement. For a temporary
- nonproduction rule, use records inside its lookback and a restricted test action group.
- Change the query time filter and portal override together, within the alert scan limit.
-3. Authorize the test rule identity, enable it, and observe a scheduled alert and notification
- in **Monitor > Alerts**. Check resolution after the condition clears, allowing for stateful
- timing. Remove the test rule/overrides and restore the intended production settings.
-4. Don't lower production thresholds for testing or route tests to production automation.
- If needed, use an approved non-phone test sign-in flow. Don't bypass Easy Auth, send live
- OTPs, induce repeated wrong codes, or alter production authentication policy.
+1. Test the saved action group and confirm the team receives it. Fix **View details** failures
+ before testing the rule; read access alone may not grant test permission.
+2. Run the saved query against healthy, qualifying, empty, and delayed windows. For a temporary
+ nonproduction rule, use existing records, a restricted action group, and an authorized rule identity.
+3. Observe one scheduled alert and its resolution, then remove temporary overrides and restore
+ the intended settings. Keep the query time filter and portal override aligned.
+4. Never lower production thresholds, trigger live OTPs, repeat wrong codes, bypass Easy Auth,
+ or change production policy for a monitoring test. Use an approved non-phone test sign-in if needed.
-### Build a Workbook when you need a shared view
+
+### Workbook
1. Review the [Microsoft monitoring repository](https://github.com/microsoft/external-id-azure-monitor)
- and its authentication, MFA, and user-behavior templates linked above. Inspect
- **serializedData** query definitions with your query maintainer. These are ARM templates,
- not text to paste wholesale into Logs. **Deploy to Azure** can create resources/delegation;
- don't select it just to inspect a query.
-2. Check tables, tenant features, personal fields, units, filters, and time settings.
- The [risk template](https://github.com/microsoft/external-id-azure-monitor/blob/main/templates/riskDetectionWorkbookDeployment.json)
- doesn't make risk features available in your tenant.
-3. Open **Azure portal > Monitor > Workbooks > New**, enter **Edit**, then **Add > Add query**.
- Select **Logs (Analytics)** and your workspace. Use the reviewed query and follow
- [Workbook authoring](https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-create-workbook)
- and [Add queries](https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-create-workbook#add-queries).
- Choose a suitable **Visualization**, **Run Query**, then **Done editing**.
-4. Label record/step counts clearly, explain missing data and arrival delay, and include
- freshness. Bind app/workspace/time parameters into every relevant query and test changes;
- selectors don't automatically replace hard-coded filters. Restrict detailed user/IP views.
-5. **Save** to the agreed subscription/resource group/location and have a teammate check
- access. Workbook sharing doesn't grant underlying log access, create alerts, or prove
- they are enabled.
+ with the query maintainer. Its templates are deployable ARM resources, not text to paste into
+ Logs; inspecting them does not require selecting **Deploy to Azure**.
+2. In **Monitor > Workbooks > New**, add the reviewed workspace query using Microsoft's
+ [Workbook authoring guide](https://learn.microsoft.com/en-us/azure/azure-monitor/visualize/workbooks-create-workbook).
+ Verify tables, licensing, filters, units, time range, freshness, and personal-data exposure.
+3. Save to the approved location and have another authorized user test access. Workbook sharing
+ does not grant log access, create alerts, or enable tenant risk features.
### Investigate alerts and manage the data
-1. In **Monitor > Alerts > selected alert**, check the rule, workspace, app/tenant scope,
- UTC window, measured value, and rule/collection health. Open the saved query for that
- period, then the relevant Entra **Basic info**, **Authentication Details**, and policy results.
-2. Separate user mistakes/interruptions, releases, and policy changes from outages or abuse.
- Escalate suspected compromise through your security response process; a heuristic isn't
- a confirmed fraud verdict.
-3. For phone concerns, compare [runtime-specific Function logs](MONITORING.md#runtime-specific-log-discovery)
- and restricted provider records. `SigninLogs.CorrelationId`, Function `OperationId`,
- request IDs, and provider references are **not guaranteed equal**. Prove an association
- before joining them. No match doesn't prove no Function call; fallback can complete
- authentication through another path. Provider acceptance isn't handset receipt.
-4. Keep raw identity/device/IP/phone data in approved systems. Check workspace/table access,
- Workbook sharing, and recipients. Don't put tokens, credentials, OTPs, or messages in
- alerts/public issues. Query filters are **not an access-control boundary**.
-5. Review **Workspace > Tables** plan, analytics retention, and total retention using
- [retention instructions](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-configure).
- Confirm saved settings cover your investigation period; Entra portal and workspace
- retention are separate.
-6. Check **Usage and estimated costs** and **Cost Management > Cost analysis** with the
- [cost guidance](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs).
- Export selects categories, not an app-only stream: query filters don't remove tenant-wide
- ingestion charges. Include retention, queries, alert frequency/dimensions, notifications,
- and separately enabled services. Provider billing is separate again.
-7. Review caps/transformations with the collection maintainer; they can remove evidence
- the rules need. Keep setup notes, test results, contacts for support/security/privacy/
- retention/costs, and escalation/maintenance arrangements together. Walk a teammate through
- the query, Workbook, rule health, and test notification without sending OTPs or changing policy.
+1. In **Monitor > Alerts**, verify the rule, workspace, tenant/app scope, UTC window, measured
+ value, and collection health. Re-run the saved query, then inspect the matching Entra details.
+2. Separate user interruption and planned changes from outages or abuse. Escalate suspected
+ compromise through the security process; a heuristic is not a fraud verdict.
+3. For phone concerns, compare [Function logs](MONITORING.md#runtime-specific-log-discovery) and
+ restricted provider records. Their correlation IDs are not guaranteed to match. Prove any
+ association; provider acceptance still is not handset receipt.
+4. Keep identity, device, IP, and phone data in approved systems. Query filters are not access
+ control. Never put tokens, credentials, OTPs, messages, or raw personal data in alerts or issues.
+5. Review [retention](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/data-retention-configure),
+ [log costs](https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs), caps,
+ transformations, alert frequency, and recipients with their owners. Keep the query, tests,
+ contacts, and escalation procedure together and walk a teammate through them.
diff --git a/dotnet/README.md b/dotnet/README.md
index 5f647df..f5ddd51 100644
--- a/dotnet/README.md
+++ b/dotnet/README.md
@@ -1,51 +1,32 @@
-# External Phone Provider Function: C# (.NET isolated worker)
+# C# implementation
-Implements the shared [contract](../docs/CONTRACT.md) with one `SendOtp` request pipeline and one
-selected provider per deployment. Target: .NET 8 isolated worker, Azure Functions v4.
+
+
+[Documentation](../docs/README.md) | [Settings](#environment-configuration) | [Request behavior](#request-behavior) | [Source](#source)
+
+Implements the shared [contract](../docs/CONTRACT.md) with one provider per deployment.
+Target: .NET 8 isolated worker, Azure Functions v4.
## Setup and deployment
-**Customer deployment:** start with the [root guide](../README.md). Guided setup builds with the
-.NET 8 SDK, publishes this runtime, and configures its Azure settings. Continue with
-[provider authentication and validation](../docs/ONBOARDING.md#complete-provider-authentication);
-do not start a local host or republish as another onboarding step.
-
-**The steps below are optional developer/manual deployment work.** Guided providers are
-Telesign and Soprano; other adapters require separate integration.
-
-For multiple regional origins behind one URL, see [manual Front Door onboarding](../docs/FRONTDOOR.md).
-No Front Door deployment script or readiness handler is supplied. The reported multi-region trials
-used JavaScript; validate an equivalent .NET readiness implementation and deployment separately.
-
-1. Follow [customer onboarding](../docs/ONBOARDING.md). Set `EPP_PROVIDER_NAME` to the selected
- adapter's `Name` (`` is only a placeholder).
-2. Consult the selected adapter in [Src/Providers/](Src/Providers/) for required credentials and
- options. For API-key authentication, store credentials in Key Vault under the provider's secret names
- and grant the Function's managed identity *Key Vault Secrets User*. For OAuth, complete the
- [provider-administrator handoff](../docs/ONBOARDING.md#soprano-provider-administrator-handoff)
- instead of creating API-key secrets. Configure the matching endpoints/options for either mode.
-3. Base private local settings on [../docs/local.settings.sample.json](../docs/local.settings.sample.json),
- replacing placeholders and selecting `FUNCTIONS_WORKER_RUNTIME=dotnet-isolated`. Put settings
- at the app root beside [host.json](host.json). Configure decryption from the
- [shared catalog](../docs/CONTRACT.md#4-configuration-app-settings--env) and caller trust through
- [Easy Auth](../docs/ONBOARDING.md#2-provision-encryption-and-deployment-trust), the only authentication
- gate before the anonymous Function. Enable `requireAuthentication=true`,
- `unauthenticatedClientAction=Return401` and `requireHttps=true`; pin the trusted tenant issuer,
- endpoint-app `allowedAudiences` and a nonempty `allowedApplications` list for the authorized Microsoft
- caller. Do not exclude SendOtp. There is no backup application token validation; never expose the
- endpoint to the public internet with Easy Auth disabled or bypassed.
-
- `SendOtp` uses Azure Functions `[FromBody]` binding to create `EntraSendOtpPayload`. Malformed JSON,
- invalid enum tokens and other deserialization failures are rejected by the Functions binding/runtime
- before `SendOtp` runs. Those failures therefore do not produce application `ILogger` events or the
- handler's custom error response body; platform diagnostics and responses apply instead.
-4. Build [dotnet.csproj](dotnet.csproj), run the offline xUnit suites in
- [tests/Epp.Otp.Tests.csproj](tests/Epp.Otp.Tests.csproj), and start the local Functions host from this folder.
- Core Tools has no Easy Auth: bind only to loopback, with no tunnels or public forwarding.
-5. Publish this app folder to a compatible .NET isolated Function App. Inspect the package: both
- [.funcignore](.funcignore) and the project protect local settings; private keys must not be included.
- Offline tests cover application behavior, not platform authentication; run the separate
- [deployed security checks](../docs/ONBOARDING.md#4-package-deploy-and-verify).
+**Sample deployment:** follow the [five-step guide](../docs/GETTING-STARTED.md), then complete
+[provider authentication and validation](../docs/ONBOARDING.md#complete-provider-authentication).
+
+**Custom package development:** follow these steps from `dotnet/`:
+
+1. Create the [local settings below](#environment-configuration). Select an adapter's fixed `Name`
+ from [Providers/](Src/Providers/), configure it, and complete
+ [provider authentication](../docs/ONBOARDING.md#complete-provider-authentication).
+2. Build [dotnet.csproj](dotnet.csproj), run [xUnit tests](tests/Epp.Otp.Tests.csproj), and start
+ Core Tools here, **on loopback only, without tunnels**; it has no Easy Auth.
+3. For manual deployment, publish to a compatible .NET isolated Function App. Inspect the output
+ for private settings and complete the
+ [Easy Auth trust checklist](../docs/ONBOARDING.md#manual-deployment-trust-checklist), then run
+ [authorized deployed checks](../docs/ONBOARDING.md#validate-the-deployed-endpoint) to verify
+ platform authentication.
+
+**Regional continuity:** use the [Front Door guide](../docs/FRONTDOOR.md) and include
+.NET-specific package, startup, dependency, and telemetry checks.
## Environment configuration
@@ -64,85 +45,61 @@ For local evaluation, start Azurite and replace the test-key placeholder in this
}
```
-For live delivery, add `EPP_PROVIDER_NAME`, `EPP_PROVIDER_SMS_ENDPOINT`,
-`EPP_PROVIDER_VOICE_ENDPOINT`, and matching provider authentication settings to `Values`.
-Each request's `channel` selects its required endpoint; the URLs may differ.
-A missing, empty, or invalid selected endpoint fails closed without fallback.
-Add `EPP_PROVIDER_ACCOUNT_NAME` and adapter-specific options only when required. Optional
-`EPP_PROVIDER_TIMEOUT_MS` is a string such as `"1500"`. Replace placeholders; store provider credentials
-under the adapter's Key Vault secret names, not in local settings. See the
-[complete variable table](../TECHNICAL.md#configure-environment-variables).
-
-Core Tools loads `Values` into environment variables. [AppConfig.Read](Src/AppConfig.cs) reads them
-through `IEnv`; direct worker execution and unit tests do not automatically load local settings.
-Restart the host after edits. Configure local host storage other than Azurite separately; do not copy
-the emulator connection into Azure. Core Tools does not resolve Key Vault references locally; supply
-the local test PEM or base64 PEM directly. The [project](dotnet.csproj) excludes private local settings
-from publish output.
-
-For Azure, configure the same application variables on the serving app/slot's **Environment variables
-→ App settings** page and resolve the private PEM through a Key Vault reference. Key Vault provider
-credentials use managed identity, not the developer's CLI login. Use loopback-only local evaluation
-or the offline tests' injected environment and secret resolver for local development.
+For live delivery, use the [shared settings table](../TECHNICAL.md#configure-environment-variables)
+for `EPP_PROVIDER_NAME`, both channel endpoints, authentication, and adapter-specific options.
+Keep values as strings, including `EPP_PROVIDER_TIMEOUT_MS`, for example `"1500"`.
+Replace placeholders and store credentials in the adapter's Key Vault secrets. Each selected
+channel requires a valid endpoint; an invalid or missing endpoint returns an error without using
+the other channel.
+
+Core Tools loads `Values` into environment variables; direct worker execution and tests do not.
+Restart after edits. Use a local test PEM because Core Tools cannot resolve Key Vault references.
+The [project](dotnet.csproj) excludes local settings from publish output.
+
+In Azure, configure the same app settings and use a Key Vault reference for the private PEM.
+Provider credentials use managed identity, not a developer CLI login. Keep local evaluation on loopback.
## Request behavior
-`POST /api/SendOtp` uses the same request and trust boundaries as the other runtimes. Incoming
-`mode`, `channel`, `ttlSeconds` and `tenantId` are request data, not deployment authentication settings.
-Easy Auth authenticates and authorizes the caller before the anonymous handler validates the payload
-and decrypts the JWE. Incoming `Authorization` is not parsed or echoed by the handler. JWE does not
-authenticate the caller: anyone with the public key can encrypt a request, and a fixed nonce is not authentication.
-Requests that bind successfully retain the application validation, PII-safe logging and response lifecycle.
-
-For any provider, use incoming `mode: 2` or `mode: "evaluation"` to evaluate without sending.
-Platform authentication on Azure, handler validation and decryption run, but provider lookup, provider Key Vault
-reads and provider HTTP do not. No provider configuration or diagnostic environment flag is required.
-Live requests use the configured provider's API key or OAuth token and
-await acceptance before returning the nonce; failures omit it. Acceptance is not handset delivery.
-Platform/key prerequisites and HTTP outcomes are defined in the
-[contract](../docs/CONTRACT.md#evaluation-no-provider-send).
-
-For Soprano voice, the adapter extracts the first six-digit passcode from the rendered message. It
-uses the request's nonblank locale as the language, falling back to `en-US`, and sends fixed gender
-`1` and loop `2`. These values require no additional environment settings. Soprano SMS continues to
-forward the rendered message unchanged.
-
-Telesign SMS also forwards the rendered message unchanged. Telesign voice comma-separates each
-six-digit numeric run that is not part of a longer number and repeats the complete paced message twice.
+`POST /api/SendOtp` follows the [shared request/response contract](../docs/CONTRACT.md#1-http-api).
+Easy Auth authenticates the caller. JWE decryption, nonce values, and request fields serve other
+parts of the contract. The handler leaves incoming `Authorization` processing to Easy Auth and
+does not echo it.
+
+**.NET binding runs first:** `[FromBody]` creates `EntraSendOtpPayload`. Malformed JSON, invalid
+enum tokens, or other deserialization errors can fail before `SendOtp`, its `ILogger` events,
+or its custom error response. Successfully bound requests retain application validation,
+privacy-safe logging, and response behavior. See [binding differences](../docs/CONTRACT.md#runtime-binding-differences).
+
+Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-send) to validate
+authentication and decryption without a provider send. Live success confirms acceptance,
+**not handset receipt**. Check a timeout before retrying.
+
+For message/voice mapping, see the [Soprano delivery contract](../docs/CONTRACT.md#encrypteddeliverycontext-jwe)
+and [Telesign integration](../TECHNICAL.md#telesign-epp).
## Source
-Start with the provider-neutral
+Use the provider-neutral
[BYOT request and response guide](../docs/BRING-YOUR-OWN-PROVIDER.md) for the Microsoft Entra ID payload,
-caller authentication, and response rules. Your team owns the outbound service integration.
+caller authentication, and response rules. Customers can connect the outbound phone service
+that meets their requirements.
For deployed diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and
[runtime-specific log discovery](../docs/MONITORING.md#runtime-specific-log-discovery).
The host's OpenTelemetry setting does not by itself initialize a direct worker exporter;
verify actual `ILogger` collection before depending on event-based alerts.
-`SendOtp` validates and decrypts the request, selects the configured provider in its private
-`SelectProvider` method, resolves credentials, and calls the provider's `SendOtpAsync`.
-That method passes its request factory and typed response mapper to
-`PhoneProviderBase.SendJsonAsync`, which builds and sends the `HttpRequestMessage`,
-reads/deserializes the response, and returns a normalized `ProviderResult`.
-`SendOtp` maps that result to the endpoint HTTP response.
-The shared transport uses redirects disabled, `ResponseHeadersRead`, and a bounded timeout.
-Each provider maps its recognized statuses to an outcome; selection uses `EPP_PROVIDER_NAME`.
-Request-aware routing or multiple accounts require a separate design for trust, configuration,
-credentials, and testing, not just a change to `SelectProvider`.
-
-`CredentialTokenService` is the hosted startup warmer and runtime credential cache.
-`EPP_KEY_VAULT_CACHE_ENABLED` controls API-key caching; `EPP_ACCESS_TOKEN_CACHE_ENABLED` controls
-OAuth caching. Both default to `true`. Setting the selected switch to `false` skips startup warmup
-and fetches on every live request, using fresh SDK credentials for OAuth.
-When enabled, it asks the selected provider for credentials at startup and on cache misses.
-Each provider owns credential acquisition and its secret names. The service stores
-the result in .NET `MemoryCache` until the credential's absolute expiry; the next request fetches a
-replacement. There is no polling timer or separate cache implementation. The fetch has a
-2.5-second cancellation budget linked to its caller's token. `MemoryCache.GetOrCreateAsync`
-does not guarantee a single concurrent fetch on cache misses; do not assume the JavaScript/Python
-shared-acquisition behavior applies here. Evaluation remains independent.
+`SendOtp` validates and decrypts, selects `EPP_PROVIDER_NAME`, resolves credentials, and calls the
+provider. `PhoneProviderBase.SendJsonAsync` owns bounded HTTP transport and typed response
+mapping; each provider maps its statuses to `ProviderResult`. Request-aware routing or multiple
+accounts require a separate trust, configuration, credential, and test design.
+
+`CredentialTokenService` warms and caches the selected provider credential. The shared cache
+switches default to `true`; disabling the selected switch skips warmup and fetches on every live
+request. Enabled credentials remain in `MemoryCache` until absolute expiry. There is no polling
+timer, and concurrent misses can fetch more than once. The fetch has a 2.5-second cancellation
+budget. See the [shared cache contract](../docs/CONTRACT.md#credential-caching-and-refresh).
| Source | Purpose |
|---|---|
@@ -152,7 +109,7 @@ shared-acquisition behavior applies here. Evaluation remains independent.
| [Src/EntraSendOtpPayload.cs](Src/EntraSendOtpPayload.cs) | Bound request model, strict channel/mode converters and semantic validation |
| [Src/JweDeliveryContext.cs](Src/JweDeliveryContext.cs) | Pinned JWE decryption and decrypted delivery context |
| [Src/CredentialTokenService.cs](Src/CredentialTokenService.cs) | Provider-supplied retrieval, one expiring `MemoryCache` value and startup warmup |
-| [Src/OtpLog.cs](Src/OtpLog.cs) | Source-generated, strongly typed [structured logging events](../docs/CONTRACT.md#application-logs) |
+| [Src/OtpLog.cs](Src/OtpLog.cs) | Source-generated, strongly typed [structured logging events](../docs/LOGGING.md#event-sequence) |
| [Src/PhoneProviderBase.cs](Src/PhoneProviderBase.cs) | Provider extension contract and shared typed JSON/HTTP transport |
| [Src/Providers/](Src/Providers/) | Provider identity, credential delegation and API-specific request/response protocols |
| [Src/SecretResolver.cs](Src/SecretResolver.cs) | Key Vault transport; `ISecretResolver.ResolveAsync` accepts cancellation |
@@ -163,6 +120,6 @@ name used by the simple `SelectProvider` policy in `SendOtp`. Providers own auth
credential resolution, request construction and private typed response DTOs. Their public `SendOtpAsync`
method delegates to the base `SendJsonAsync` transport and maps the typed response into
`ProviderResult`; provider mapping code never handles `HttpResponseMessage` or raw JSON DOM types.
-Provider-specific response semantics stay in each provider; no shared response-mapping
-registry is used. Common async HTTP transport lives in `PhoneProviderBase`. See
-[production limitations](../docs/CONTRACT.md#production-limitations) before production use.
+Each provider keeps its own response rules. Common async HTTP transport lives in
+`PhoneProviderBase`. See
+[production-readiness considerations](../docs/CONTRACT.md#production-limitations) when planning a live service.
diff --git a/javascript/README.md b/javascript/README.md
index 460301f..d903412 100644
--- a/javascript/README.md
+++ b/javascript/README.md
@@ -1,47 +1,31 @@
-# External Phone Provider Function: JavaScript
+# JavaScript implementation
-A Node.js Azure Function implementing the shared [contract](../docs/CONTRACT.md). The Function owns
-an explicit parse → decrypt → evaluation short-circuit or provider selection → credential resolution
-→ provider request/transport/result → response flow. One provider is selected per deployment, and
-each provider owns its credentials, wire request, response interpretation, outcome and failure class.
+
+
+[Documentation](../docs/README.md) | [Settings](#environment-configuration) | [Request behavior](#request-behavior) | [Source](#source-and-extension-points)
+
+A Node.js Azure Function implementing the shared [HTTP contract](../docs/CONTRACT.md) with one
+provider per deployment.
## Setup
-**Customer deployment:** start with the [root guide](../README.md). Guided setup publishes this
-runtime and its Azure settings; continue with [provider authentication and validation](../docs/ONBOARDING.md#complete-provider-authentication).
-Do not create local settings or republish after a successful guided deployment.
-
-**The steps below are optional developer/manual deployment work**, not additional onboarding
-requirements. Guided providers are Telesign and Soprano; other adapters require separate integration.
-
-For multiple regional origins behind one URL, see [manual Front Door onboarding](../docs/FRONTDOOR.md).
-The JavaScript evaluation trials used a separate readiness handler; it is not included in this
-sample's release package. No Front Door deployment script is supplied.
-
-1. Follow [customer onboarding](../docs/ONBOARDING.md). Choose a bundled provider and set
- `EPP_PROVIDER_NAME` to its fixed id; `` is a placeholder, not a default.
-2. Consult the selected implementation in [src/functions/providers/](src/functions/providers/)
- for required credentials and options. For API-key authentication, store credential values under
- the provider's Key Vault secret names and grant the Function's managed identity *Key Vault Secrets User*.
- For OAuth, complete the [provider-administrator handoff](../docs/ONBOARDING.md#soprano-provider-administrator-handoff)
- instead of creating API-key secrets. Configure the matching endpoints and required options for either mode.
- This guide does not duplicate individual API contracts.
-3. Use [../docs/local.settings.sample.json](../docs/local.settings.sample.json) as a starting point,
- replacing placeholders with the selected provider's settings. Keep local settings private at
- the app root beside [host.json](host.json), with `FUNCTIONS_WORKER_RUNTIME=node`.
-4. Configure decryption from the [shared catalog](../docs/CONTRACT.md#4-configuration-app-settings--env).
- Follow [platform trust setup](../docs/ONBOARDING.md#2-provision-encryption-and-deployment-trust): Easy
- Auth is the only caller-authentication gate before the anonymous Function. Enable
- `requireAuthentication=true`, `unauthenticatedClientAction=Return401` and `requireHttps=true`; pin the
- trusted tenant issuer, endpoint-app `allowedAudiences` and a nonempty `allowedApplications` list for
- the authorized Microsoft caller. Do not exclude SendOtp. There is no backup application token validation;
- never expose the endpoint to the public internet with Easy Auth disabled or bypassed.
-5. Install dependencies from [package.json](package.json), run its offline test script and start the
- local Functions host from this folder. Core Tools has no Easy Auth: bind only to loopback, with no
- tunnels or public forwarding. Publish this app folder only, preserving dependencies and observing
- [.funcignore](.funcignore); inspect the package before upload. Offline tests cover application
- behavior, not platform authentication; run the separate
- [deployed security checks](../docs/ONBOARDING.md#4-package-deploy-and-verify).
+**Sample deployment:** follow the [five-step guide](../docs/GETTING-STARTED.md), then complete
+[provider authentication and validation](../docs/ONBOARDING.md#complete-provider-authentication).
+
+**Custom package development:** follow these steps from `javascript/`:
+
+1. Create the [local settings below](#environment-configuration). Select a fixed provider ID from
+ [providers/](src/functions/providers/), configure it, and complete
+ [provider authentication](../docs/ONBOARDING.md#complete-provider-authentication).
+2. Install [package.json](package.json) dependencies, run its offline test script, and start Core Tools
+ from this folder. **Bind only to loopback, without tunnels:** Core Tools has no Easy Auth.
+3. For manual deployment, [package](../TECHNICAL.md#build-zips-locally) this app, inspect it for
+ secrets, and complete the [Easy Auth trust checklist](../docs/ONBOARDING.md#manual-deployment-trust-checklist).
+ Keep Easy Auth enabled for SendOtp, then run
+ [authorized deployed checks](../docs/ONBOARDING.md#validate-the-deployed-endpoint) to verify
+ platform authentication.
+
+**Regional continuity:** use the [Front Door guide](../docs/FRONTDOOR.md).
## Environment configuration
@@ -61,77 +45,51 @@ the test-key placeholder in this minimal setup:
}
```
-For live delivery, add `EPP_PROVIDER_NAME`, `EPP_PROVIDER_SMS_ENDPOINT`,
-`EPP_PROVIDER_VOICE_ENDPOINT`, and matching provider authentication settings to `Values`.
-Each request's `channel` selects its required endpoint; the URLs may differ.
-A missing, empty, or invalid selected endpoint fails closed without fallback.
-Add `EPP_PROVIDER_ACCOUNT_NAME` and any provider-specific options only when required. Optional
-`EPP_PROVIDER_TIMEOUT_MS` is a string such as `"1500"`. Replace placeholders; do not put API keys in
-this file. See the [complete variable table](../TECHNICAL.md#configure-environment-variables).
-
-Core Tools copies `Values` into the process environment; direct Node processes and the offline tests
-do **not** automatically load this file. [AppConfig](src/functions/config.js) reads `process.env`
-once per call to `readConfig()`. Restart the host after changing settings. Configure any local host
-storage other than Azurite separately; do not copy a local emulator connection into Azure. Core Tools
-does not resolve Key Vault references locally; supply the local test PEM or base64 PEM directly.
-
-Use `EPP_PROVIDER_NAME`, the channel-specific endpoint settings above, and `EPP_PROVIDER_TIMEOUT_MS`;
-configure caller authentication in Easy Auth, not application settings. Add only provider options
-read by the selected adapter, such as a service-plan ID.
-
-The Soprano provider uses the configured complete endpoint and an OAuth bearer token. For voice, it
-extracts the first six-digit passcode from the rendered message and sends fixed synthesis values:
-gender `1` and loop `2`. It uses the request's nonblank locale as the language, falling back to
-`en-US` when the locale is absent or invalid. These values require no additional environment
-settings. Soprano SMS continues to forward the rendered message unchanged. The Telesign provider
-uses the configured complete endpoint and API-key credentials from Key Vault. Telesign SMS forwards
-the rendered message unchanged; Telesign voice comma-separates each six-digit numeric run that is
-not part of a longer number and repeats the complete paced message twice.
-
-For Azure, set these application variables on the Function App/slot's **Environment variables → App
-settings** page and use a Key Vault reference for the private PEM. The provider-secret resolver uses
-managed identity; signing into the CLI locally does not supply that identity. Local evaluation avoids
-provider lookup, while tests inject mocked credentials and HTTP. Keep the local endpoint on loopback.
+For live delivery, use the [shared settings table](../TECHNICAL.md#configure-environment-variables)
+for `EPP_PROVIDER_NAME`, both channel endpoints, authentication, and adapter-specific options.
+Keep `Values` as strings, including `EPP_PROVIDER_TIMEOUT_MS`, for example `"1500"`.
+Replace placeholders; Store API keys in Key Vault. Each selected channel requires a valid endpoint; an invalid or
+missing endpoint returns an error without using the other channel.
+
+Core Tools copies `Values` into the process environment; direct Node processes and offline tests
+do not load the file. Restart after edits. Use a local test PEM because Core Tools cannot resolve
+Key Vault references locally. Keep Azurite settings local.
+
+In Azure, configure the same app settings and use a Key Vault reference for the private PEM.
+Provider secrets use managed identity, not a developer CLI login. Keep local evaluation on loopback.
## Request behavior
-Easy Auth authenticates and authorizes the caller before `POST /api/SendOtp`; the anonymous handler
-validates the envelope and decrypts the JWE, without parsing or echoing incoming `Authorization`.
-JWE does not authenticate the caller: anyone with the public key can encrypt a request, and a fixed nonce
-is not authentication. Request `mode`, `channel`, `ttlSeconds` and `tenantId` are request data, not
-environment settings or sources of identity trust. SMS forwards the caller-rendered message;
-voice uses the provider-specific extraction/pacing described above.
+`POST /api/SendOtp` follows the [shared request/response contract](../docs/CONTRACT.md#1-http-api).
+Easy Auth authenticates the caller. JWE decryption, nonce values, and request fields serve other
+parts of the contract. The handler leaves incoming `Authorization` processing to Easy Auth and
+does not echo it.
-For non-delivery validation, use incoming `mode: 2` or `mode: "evaluation"`. This evaluation path
-works for every provider without provider configuration, provider Key Vault reads or provider HTTP;
-platform authentication on Azure and handler decryption still run. No diagnostic environment flag is needed. See the
-[evaluation contract](../docs/CONTRACT.md#evaluation-no-provider-send) for authentication/key prerequisites.
+Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-send) to validate
+authentication and decryption without a provider send. Live success confirms acceptance,
+**not handset receipt**. Check a timeout before retrying.
-Live requests use the configured provider's API key or OAuth token and await acceptance before returning the nonce.
-Acceptance is not handset delivery; failures omit the nonce, and timeouts must not trigger blind
-retries. The shared contract defines validation, HTTP outcomes and privacy-safe logging.
+For message/voice mapping and credential rules, see the
+[Soprano delivery contract](../docs/CONTRACT.md#encrypteddeliverycontext-jwe) and
+[Telesign integration](../TECHNICAL.md#telesign-epp).
## Source and extension points
-Start with the provider-neutral
+Use the provider-neutral
[BYOT request and response guide](../docs/BRING-YOUR-OWN-PROVIDER.md) for the Microsoft Entra ID payload,
-caller authentication, and response rules. Your team owns the outbound service integration.
+caller authentication, and response rules. Customers can connect the outbound phone service
+that meets their requirements.
-For deployed diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and the
-[JavaScript service-event queries](../docs/MONITORING.md#4-javascript-service-event-queries).
-Logs are JSON `service` events ending in `request_completed`, not a `logType: "request"` summary.
+For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and the
+[JavaScript event queries](../docs/MONITORING.md#4-javascript-service-event-queries).
-`EPP_KEY_VAULT_CACHE_ENABLED` controls API-key caching; `EPP_ACCESS_TOKEN_CACHE_ENABLED` controls
-OAuth caching. Both default to `true`. Setting the selected switch to `false` acquires credentials
-per live request, with no startup preparation, polling, or cross-request reuse.
+The [cache switches and defaults](../docs/CONTRACT.md#credential-caching-and-refresh) apply here.
+Disabling the selected cache removes startup preparation, polling, and cross-request reuse,
+not live credential acquisition.
-With caching enabled, the app-start hook selects `ApiKeyCache` or `AccessTokenCache` from the provider credential spec.
-Only that cache starts: API keys use Key Vault and `lru-cache`; access tokens use the MI/Entra SDKs,
-without Key Vault. One shared 30-second refresh loop and one in-flight acquisition keep warm reads
-nonblocking. Configuration changes require restart; failures never extend expiry. A small HTTP-client
-wrapper propagates cancellation to the installed identity SDK. Shutdown prevents late publication. See the
-[refresh contract](../docs/CONTRACT.md#credential-caching-and-refresh). Leave the provider unset for
-local evaluation-only use without credential acquisition; prewarming never sends an OTP.
+With caching enabled, API keys use Key Vault and `lru-cache`; OAuth uses the managed-identity/Entra
+SDKs. A 30-second refresh loop and one in-flight acquisition support warm reads. See the
+[refresh contract](../docs/CONTRACT.md#credential-caching-and-refresh).
| Source | Purpose |
|---|---|
@@ -148,8 +106,8 @@ local evaluation-only use without credential acquisition; prewarming never sends
| [test/](test/) | Representative offline checks |
To add a provider, implement its fixed `name`, `authenticationMode`, `credentialSpec`,
-`createRequest` and `interpretResponse` in the provider folder, then add it to the switch in
+`createRequest`, and `interpretResponse` in the provider folder. Then add it to the switch in
[providers/index.js](src/functions/providers/index.js). `interpretResponse` returns a
`ProviderResult` containing the provider-owned outcome, endpoint HTTP status and one safe fixed
failure classification. Raw API JSON stays inside the provider. See
-[production limitations](../docs/CONTRACT.md#production-limitations) before production use.
+[production-readiness considerations](../docs/CONTRACT.md#production-limitations) when planning a live service.
diff --git a/python/README.md b/python/README.md
index 8a2e810..7e4b0b9 100644
--- a/python/README.md
+++ b/python/README.md
@@ -1,46 +1,32 @@
-# External Phone Provider Function: Python (v2 model)
+# Python implementation
-Implements the shared [contract](../docs/CONTRACT.md) with a direct Azure Function flow and one
-selected provider per deployment. Target: Python 3.11, Azure Functions v4, Python v2 programming model.
+
+
+[Documentation](../docs/README.md) | [Settings](#environment-configuration) | [Request behavior](#request-behavior) | [Source](#source)
+
+Implements the shared [contract](../docs/CONTRACT.md) with one provider per deployment.
+Target: Python 3.11, Azure Functions v4, Python v2 programming model.
## Setup and deployment
-**Customer deployment:** start with the [root guide](../README.md). Guided setup performs the
-Azure remote build, publishes this runtime, and configures its Azure settings. Continue with
-[provider authentication and validation](../docs/ONBOARDING.md#complete-provider-authentication);
-do not install Python locally or republish as another onboarding step.
-
-**The steps below are optional developer/manual deployment work.** Guided providers are
-Telesign and Soprano; other adapters require separate integration.
-
-For multiple regional origins behind one URL, see [manual Front Door onboarding](../docs/FRONTDOOR.md).
-No Front Door deployment script or readiness handler is supplied. The reported multi-region trials
-used JavaScript; validate an equivalent Python readiness implementation and deployment separately.
-
-1. Follow [customer onboarding](../docs/ONBOARDING.md). Set `EPP_PROVIDER_NAME` to the selected
- provider id (`` is only a placeholder).
-2. Consult the selected provider in [src/providers/](src/providers/) for required credentials and
- options. For API-key authentication, store credentials in Key Vault under the provider-owned secret names
- and grant the Function's managed identity *Key Vault Secrets User*. For OAuth, complete the
- [provider-administrator handoff](../docs/ONBOARDING.md#soprano-provider-administrator-handoff)
- instead of creating API-key secrets. Configure the matching endpoints/options for either mode.
-3. Base private local settings on [../docs/local.settings.sample.json](../docs/local.settings.sample.json),
- replacing placeholders and selecting `FUNCTIONS_WORKER_RUNTIME=python`. Put settings at the
- app root beside [host.json](host.json). Configure decryption from the
- [shared catalog](../docs/CONTRACT.md#4-configuration-app-settings--env) and caller trust through
- [Easy Auth](../docs/ONBOARDING.md#2-provision-encryption-and-deployment-trust), the only authentication
- gate before the anonymous Function. Enable `requireAuthentication=true`,
- `unauthenticatedClientAction=Return401` and `requireHttps=true`; pin the trusted tenant issuer,
- endpoint-app `allowedAudiences` and a nonempty `allowedApplications` list for the authorized Microsoft
- caller. Do not exclude SendOtp. There is no backup application token validation; never expose the
- endpoint to the public internet with Easy Auth disabled or bypassed.
-4. Use a virtual environment, install [requirements.txt](requirements.txt) and pytest, then run the
- offline [tests/](tests/) from this folder. Start the local Functions host from this app root.
- Core Tools has no Easy Auth: bind only to loopback, with no tunnels or public forwarding.
-5. Publish this folder to a compatible Linux Python Function App with dependencies or a supported
- remote build. Inspect the package and apply [.funcignore](.funcignore); keep local settings and keys private.
- Offline tests cover application behavior, not platform authentication; run the separate
- [deployed security checks](../docs/ONBOARDING.md#4-package-deploy-and-verify).
+**Sample deployment:** follow the [five-step guide](../docs/GETTING-STARTED.md), then complete
+[provider authentication and validation](../docs/ONBOARDING.md#complete-provider-authentication).
+
+**Custom package development:** follow these steps from `python/`:
+
+1. Create the [local settings below](#environment-configuration). Select a fixed provider ID from
+ [providers/](src/providers/), configure it, and complete
+ [provider authentication](../docs/ONBOARDING.md#complete-provider-authentication).
+2. Use a virtual environment, install [requirements.txt](requirements.txt) and pytest, and run
+ [tests/](tests/). Start Core Tools here, **on loopback only, without tunnels**; it has no Easy Auth.
+3. For manual deployment, publish to a compatible Linux Python Function with dependencies or
+ remote build. Inspect the package for private settings, complete the
+ [Easy Auth trust checklist](../docs/ONBOARDING.md#manual-deployment-trust-checklist), and run
+ [authorized deployed checks](../docs/ONBOARDING.md#validate-the-deployed-endpoint) to verify
+ platform authentication.
+
+**Regional continuity:** use the [Front Door guide](../docs/FRONTDOOR.md) and include
+Python-specific package, startup, dependency, and telemetry checks.
## Environment configuration
@@ -59,74 +45,51 @@ For local evaluation, start Azurite and replace the test-key placeholder in this
}
```
-For live delivery, add `EPP_PROVIDER_NAME`, `EPP_PROVIDER_SMS_ENDPOINT`,
-`EPP_PROVIDER_VOICE_ENDPOINT`, and matching provider authentication settings to `Values`.
-Each request's `channel` selects its required endpoint; the URLs may differ.
-A missing, empty, or invalid selected endpoint fails closed without fallback.
-Add `EPP_PROVIDER_ACCOUNT_NAME` and any adapter-specific options only when required. Keep values as
-strings, including optional `EPP_PROVIDER_TIMEOUT_MS: "1500"`. Replace placeholders; provider API
-keys belong in the provider-named Key Vault secrets, not this file. See the
-[complete variable table](../TECHNICAL.md#configure-environment-variables).
-
-Core Tools loads `Values` into `os.environ`. Direct Python execution and pytest do not automatically
-read local settings. [read_config](src/config.py) returns an `AppConfig` object; the handler/engine
-use attributes such as `config.provider_name`, not dictionary key lookups. Restart the host after
-settings change. Configure local host storage other than Azurite separately; do not copy the emulator
-connection into Azure. Core Tools does not resolve Key Vault references locally; supply the local test
-PEM or base64 PEM directly.
-
-For Azure, set the same application variables on the serving app/slot's **Environment variables → App
-settings** page. Use a Key Vault reference for the private PEM. Provider secrets require managed
-identity, which is not supplied by a developer's CLI login. Use local evaluation or the mocked offline
-tests on an ordinary workstation, and bind local hosts only to loopback.
+For live delivery, use the [shared settings table](../TECHNICAL.md#configure-environment-variables)
+for `EPP_PROVIDER_NAME`, both channel endpoints, authentication, and adapter-specific options.
+Keep values as strings, including `EPP_PROVIDER_TIMEOUT_MS: "1500"`. Replace placeholders;
+Store API keys in provider-named Key Vault secrets. Each selected channel requires a valid
+endpoint; an invalid or missing endpoint returns an error without using the other channel.
-## Request behavior
+Core Tools loads `Values` into `os.environ`; direct Python and pytest do not. Restart after edits.
+Use a local test PEM because Core Tools cannot resolve Key Vault references locally. Keep Azurite
+settings local.
+
+In Azure, configure the same app settings and use a Key Vault reference for the private PEM.
+Provider secrets use managed identity, not a developer CLI login. Keep local evaluation on loopback.
-`POST /api/SendOtp` uses the same request and trust boundaries as the other runtimes. Incoming
-`mode`, `channel`, `ttlSeconds` and `tenantId` are request data, not deployment authentication settings.
-Easy Auth authenticates and authorizes the caller before the anonymous handler validates the envelope
-and decrypts the JWE. Incoming `Authorization` is not parsed or echoed by the handler. JWE does not
-authenticate the caller: anyone with the public key can encrypt a request, and a fixed nonce is not authentication.
+## Request behavior
-For any provider, use incoming `mode: 2` or `mode: "evaluation"` to evaluate without sending.
-Platform authentication on Azure, handler validation and decryption run, but provider lookup, provider Key Vault
-reads and provider HTTP do not. No provider configuration or diagnostic environment flag is required.
-Live requests use the configured provider's API key or OAuth token and
-await acceptance before returning the nonce; failures omit it. Acceptance is not handset delivery.
-Platform/key prerequisites and HTTP outcomes are defined in the
-[contract](../docs/CONTRACT.md#evaluation-no-provider-send).
+`POST /api/SendOtp` follows the [shared request/response contract](../docs/CONTRACT.md#1-http-api).
+Easy Auth authenticates the caller. JWE decryption, nonce values, and request fields serve other
+parts of the contract. The handler leaves incoming `Authorization` processing to Easy Auth and
+does not echo it.
-For Soprano voice, the adapter extracts the first six-digit passcode from the rendered message. It
-uses the request's nonblank locale as the language, falling back to `en-US`, and sends fixed gender
-`1` and loop `2`. These values require no additional environment settings. Soprano SMS continues to
-forward the rendered message unchanged.
+Use [`mode: 2` or `"evaluation"`](../docs/CONTRACT.md#evaluation-no-provider-send) to validate
+authentication and decryption without a provider send. Live success confirms acceptance,
+**not handset receipt**. Check a timeout before retrying.
-Telesign SMS also forwards the rendered message unchanged. Telesign voice comma-separates each
-six-digit numeric run that is not part of a longer number and repeats the complete paced message twice.
+For message/voice mapping, see the [Soprano delivery contract](../docs/CONTRACT.md#encrypteddeliverycontext-jwe)
+and [Telesign integration](../TECHNICAL.md#telesign-epp).
## Source
-Start with the provider-neutral
+Use the provider-neutral
[BYOT request and response guide](../docs/BRING-YOUR-OWN-PROVIDER.md) for the Microsoft Entra ID payload,
-caller authentication, and response rules. Your team owns the outbound service integration.
+caller authentication, and response rules. Customers can connect the outbound phone service
+that meets their requirements.
-For deployed diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and
+For diagnostics, use [Application Insights](../docs/APPLICATION-INSIGHTS.md) and
[runtime-specific log discovery](../docs/MONITORING.md#runtime-specific-log-discovery).
-Python emits fixed messages with `event_name` and other logging extras, not a universal JSON
-request summary. Verify which properties the host/export pipeline preserves.
-
-`EPP_KEY_VAULT_CACHE_ENABLED` controls API-key caching; `EPP_ACCESS_TOKEN_CACHE_ENABLED` controls
-OAuth caching. Both default to `true`. Setting the selected switch to `false` acquires credentials
-per live request, with no startup preparation, polling, or cross-request reuse.
-
-With caching enabled, worker initialization selects `ApiKeyCache` or `AccessTokenCache` from the provider's credential specification.
-Only the selected cache starts: API keys use Key Vault and `cachetools.TTLCache`; access tokens use
-the MI/Entra SDKs without Key Vault. One daemon loop polls every 30 seconds. Configuration changes
-require restart. Callers can stop waiting without abandoning shared reads; synchronous SDK I/O uses connect/read
-timeouts, not a total transport deadline. `atexit` stops refresh and releases waiters; unfinished
-daemon reads cannot publish or block process exit. See the
-[refresh contract](../docs/CONTRACT.md#credential-caching-and-refresh). Leave the provider unset for
-local evaluation without background credential acquisition.
+Verify which Python logging properties the host/export pipeline preserves.
+
+The [cache switches and defaults](../docs/CONTRACT.md#credential-caching-and-refresh) apply here.
+Disabling the selected cache removes startup preparation, polling, and cross-request reuse,
+not live credential acquisition.
+
+With caching enabled, API keys use Key Vault and `cachetools.TTLCache`; OAuth uses the
+managed-identity/Entra SDKs. A daemon polls every 30 seconds. See the
+[refresh contract](../docs/CONTRACT.md#credential-caching-and-refresh).
| Source | Purpose |
|---|---|
@@ -143,4 +106,4 @@ local evaluation without background credential acquisition.
Add a provider by subclassing `PhoneProviderBase`, declaring its credential specification, and
implementing `build_request` and `map_response`. Return `ProviderResult` with the coarse endpoint
outcome and a fixed safe failure classification. Raw provider JSON remains local to the provider.
-See [production limitations](../docs/CONTRACT.md#production-limitations) before production use.
+See [production-readiness considerations](../docs/CONTRACT.md#production-limitations) when planning a live service.
diff --git a/setup/docs/README.md b/setup/docs/README.md
index 2e74f09..b1ac358 100644
--- a/setup/docs/README.md
+++ b/setup/docs/README.md
@@ -1,50 +1,41 @@
# EPP endpoint setup reference
-New to the sample? Follow the [main setup guide](../../README.md). This reference explains the
-permissions, hosting choices, script prompts, and maintenance steps in more detail.
+[Documentation](../../docs/README.md) | [First deployment](../../docs/GETTING-STARTED.md) | [Troubleshooting](Troubleshooting.md)
-The three stages in this reference are:
+This reference covers setup choices, reruns, and maintenance. For a first deployment, follow the
+five-step guide above; it handles the required source and Bicep files.
-1. [Register a dedicated application](#step-1---manually-create-the-application).
-2. [Run the setup script](#step-2---download-and-run-one-script), after checking
- [prerequisites](#prerequisites-for-step-2). It downloads its support files and deploys the
- resources and code; no repository clone or manual Bicep download is needed.
-3. [Validate and activate policy](#step-3---manually-validate-and-activate-policy), with your
- onboarding owner and policy administrator. The script does not activate policy.
+| Task | Section |
+| --- | --- |
+| Prepare access, tools, and hosting | [Prerequisites](#prerequisites-for-step-2), [permissions](#permissions-are-separate), [plans](#service-plan-selection) |
+| Register the dedicated app | [Step 1](#step-1---manually-create-the-application) |
+| Run or review setup | [Step 2](#step-2---download-and-run-one-script), [certificate choice](#encryption-certificate-selection), [cancel at approval](#reviewing-the-plan-without-deploying) |
+| Use known inputs and versions | [Noninteractive example](#noninteractive-example), [source versioning](#source-versioning) |
+| Find deployment results | [Summary fields](#read-the-deployment-summary) |
+| Validate, activate, and maintain | [Step 3](#step-3---manually-validate-and-activate-policy), [renewal](#encryption-certificate-lifecycle), [rollback](#rollback-and-decommissioning) |
+| Develop setup changes | [Offline checks](#offline-setup-checks) |
-Confirm [provider access and the approved Microsoft procedure](../../docs/ONBOARDING.md#before-purchasing-or-deploying)
-before purchasing or deploying. A provider purchase alone does not enable the tenant feature or authorize its API.
+Confirm [provider access and Microsoft's approved procedure](../../docs/ONBOARDING.md#before-purchasing-or-deploying)
+before deployment.
-Setup creates **one endpoint in one Azure region**. For multiple regions, use the separate
-[manual Front Door guide](../../docs/FRONTDOOR.md). Running setup again in another region does
-not coordinate encryption keys or Front Door origins.
+Setup creates **one regional endpoint**. See [Front Door](../../docs/FRONTDOOR.md) for regional continuity.
## Availability
-Choose a **Global or EU provider route** (the prompt calls it **Tenant scope**),
-**Telesign or Soprano**, and an
-Azure Function **platform**: Node.js, .NET, or Python, plus a **service plan**: Flex Consumption FC1
-or Premium EP1. By default, setup resolves the source repository's latest stable
-`epp-packages-*` release produced by CI. A public fork's test branch can use its matching fork release.
-There is no package URL or checksum to enter. Setup verifies `SHA256SUMS.txt` automatically and
-performs the required build and publication for the selected language.
-The Function endpoint accepts both SMS and voice; the request body's `channel` selects its provider URL.
-Setup does not prompt for a channel or write a channel restriction to app settings.
-
-Provider profiles contain complete region route objects at `deployment.routes.global` and
-`deployment.routes.eu`. Each route has separate `smsEndpoint` and `voiceEndpoint` URLs plus shared
-authentication and timing settings. The URLs can differ; setup always provisions both as
-`EPP_PROVIDER_SMS_ENDPOINT` and `EPP_PROVIDER_VOICE_ENDPOINT`. Telesign contains its supplied
-route URLs, tenant, authentication, and timings. Soprano contains its provider tenant, Global/EU
-routes, API application ID, scope, authentication, and timings. The provider files contain the
-complete deployment contract.
-
-Use a Function package that supports these channel-specific settings. Both URLs are configured
-explicitly; neither channel uses a shared endpoint fallback. Older packages must be upgraded.
+Choose **Telesign or Soprano**, **Global/EU** provider routing (prompt: **Tenant scope**),
+**Node.js/.NET/Python**, and **FC1/EP1** hosting. Setup verifies `SHA256SUMS.txt`, builds as needed,
+and publishes; no package URL/hash input is needed. See [source/package selection](#source-versioning).
+
+Both channels are configured. Profile objects `deployment.routes.global` and
+`deployment.routes.eu` define `smsEndpoint`, `voiceEndpoint`, tenant, authentication, and timing
+values. Setup writes the endpoints to `EPP_PROVIDER_SMS_ENDPOINT` and
+`EPP_PROVIDER_VOICE_ENDPOINT`; Soprano also supplies its API app ID and scope.
+
+Use a package that supports channel-specific settings; neither channel falls back to the other URL.
The current profiles use identical Global/EU URLs, and Soprano uses the same app ID/scope.
-This label is not proof of data residency or a choice of Azure region; confirm processing/routing
-with the provider. Infobip/Sinch are bundled adapters but have no guided deployment profiles.
+The route label is not a residency or Azure-region selection. Infobip and Sinch are bundled
+adapters without guided profiles.
## Service plan selection
@@ -55,125 +46,97 @@ Setup offers these two Linux hosting plans, with the same cache app settings for
| 1. Flex Consumption (FC1) | On-demand with a free usage grant; zero always-ready instances, 2048 MB, up to 40 on-demand instances | `"false"` | `"false"` |
| 2. Premium (EP1) | Existing Premium configuration with one warm instance | `"true"` | `"true"` |
-**FC1 is not an always-free deployment.** Its on-demand compute has a
+FC1 on-demand compute has a
[free usage grant](https://learn.microsoft.com/azure/azure-functions/flex-consumption-plan#billing);
usage beyond the grant, Key Vault, storage, telemetry, and provider services can incur charges.
-FC1 can scale to zero and cold-start; these settings do not remove cold-start latency or force a
-new worker on every invocation.
+FC1 can scale to zero and cold-start.
-EP1 allocates paid warm capacity even without OTP traffic. Set an Azure Cost Management budget
-and notify the resource owner; a budget notification does not stop spending. Include storage,
-Key Vault, telemetry ingestion/retention, and the provider's charges, not just Function executions.
-Stopping a Function does not necessarily stop its plan or supporting-service charges.
+EP1 allocates paid warm capacity without OTP traffic. Budget for the plan, storage, Key Vault,
+telemetry, and provider services. Budget alerts do not stop spending.
-**Deploy a Function package that supports these switches.** Older packages ignore them.
-Each switch independently controls its provider credential cache; unset defaults to `true`.
-With the selected cache disabled, live requests acquire credentials on demand without startup
-preparation, polling, or cross-request reuse. Values must be trimmed, case-insensitive `true` or
-`false`; restart after edits. Rerunning setup restores the selected plan's values.
-The decryption-key Key Vault reference remains platform-managed.
+Each switch controls one provider credential cache; unset defaults to `true`. Disabled caches
+acquire credentials per live request. Values are trimmed, case-insensitive `true` or `false`;
+restart after edits. Rerunning setup restores the plan defaults. Older packages ignore these switches.
-Use `-ServicePlan FC1` or `-ServicePlan EP1` to skip the prompt. Unattended setup requires this
-parameter; it never silently chooses a paid plan. The approval and deployment summary include
-the selected plan and both cache settings.
+Use `-ServicePlan FC1` or `-ServicePlan EP1` to skip the prompt. Unattended setup requires it.
**Use a different resource prefix to change plans.** In-place FC1/EP1 migration is not supported.
Preflight rejects a mismatch with the existing resource-group tag or actual hosting-plan SKU,
including older EP1 deployments without the new tag. Existing EP1 deployments can be rerun with
`-ServicePlan EP1`.
-For an active endpoint, onboard a new dedicated Step 1 application as well, or coordinate an
-encryption-key continuity migration with its owner. A new prefix creates a new vault, but reuse of
-the application still requires an explicit certificate decision. Select the existing certificate
-and its Key Vault location to retain key continuity; replacement affects all endpoints sharing the
-application. Setup does not automatically migrate keys or activate the replacement endpoint.
+For an active endpoint, use a new Step 1 app or coordinate key continuity. Reusing an app still
+requires an explicit certificate choice; replacement affects every endpoint sharing the app.
## Step 1 - manually create the application
-Use a dedicated nonproduction tenant/subscription for the first deployment.
+For the first deployment, use a nonproduction tenant and subscription.
1. In the customer tenant's **Microsoft Entra admin center > App registrations**, register a
- dedicated application with **Accounts in this organizational directory only** as its initial
+ new application for this endpoint with **Accounts in this organizational directory only** as its initial
supported account type; setup changes it to organizational multi-tenant after approval.
- No redirect URI, client secret, API permission, app role,
- or enterprise-application configuration is required manually.
+ No redirect URI, client secret, API permission, or app role is required manually.
2. Record the **Directory (tenant) ID** and **Application (client) ID**. The script requires the
client ID, not the application's object ID, and will not create a replacement registration.
-3. Complete provider purchase and account/sender registration, and arrange API onboarding for the selected adapter.
+3. Complete provider purchase, sender registration, and API onboarding.
Telesign uses `telesign-api-key` and `telesign-customer-id` in Key Vault. Soprano uses OAuth
client-assertion exchange with the selected provider tenant/scope/application ID. Setup does not
grant provider API consent or application roles. Finish the
[provider authentication handoff](../../docs/ONBOARDING.md#complete-provider-authentication)
after setup creates the vault and identity identifiers needed for those steps.
-After the single Step 2 approval, PowerShell makes the dedicated app organizational multi-tenant,
-restricts it through the Entra allowed-tenants preview to its home tenant plus the selected provider
-tenant from the provider JSON, preserving any existing allowed provider tenants,
-adds the `Epp.Invoke` application permission, creates/reuses its enterprise application, requires
-assignment, creates/reuses the Microsoft phone-provider service principal, and assigns `Epp.Invoke`.
-It also grants that Microsoft service principal tenant-wide Microsoft Graph `Application.Read.All`,
-adds the hostname-based identifier URI and public JWE encryption certificate, and configures Easy
-Auth to allow only the Microsoft phone-provider application. Soprano additionally creates the
-disclosed outbound managed-identity federated credential.
+Setup configures:
+
+- Organizational multitenancy and the Microsoft Entra ID allowed-tenants preview: home tenant,
+ selected provider tenant, and preserved existing allowed provider tenants.
+- The app's enterprise application with assignment required, `Epp.Invoke`, and its assignment to
+ the Microsoft phone-provider service principal.
+- That Microsoft principal's tenant-wide Graph `Application.Read.All`, identifier URI, public JWE certificate, and Easy Auth caller restriction.
+- The disclosed outbound managed-identity federated credential for Soprano.
## Prerequisites for Step 2
-- **Azure Cloud Shell in PowerShell mode, or PowerShell 7+** with an Azure CLI user login.
- Certificate issuance happens inside Key Vault without local certificate-store cmdlets.
-- Azure CLI **2.60.0+ for FC1** or **2.48.1+ for EP1** on `PATH`, with access to GitHub, Azure, Microsoft Graph, and Key Vault.
- Setup installs the Azure CLI Bicep component after confirmation when it is missing. Azure CLI itself
- must be installed before running the script. FC1 publication and EP1 Python builds also need
- network access to SCM.
-- An Azure **user** account permitted to deploy at subscription scope, create the listed resources,
- and create the scoped Azure role assignments. Bicep grants the operator **Key Vault Certificates
- Officer** for issuance and **Key Vault Secrets Officer** for provider credentials,
- scoped to this deployment's vault.
-- **Certificate reuse:** the operator needs ARM read access to the source vault, certificate-read
- access, and secret-version metadata-read access before approval. For a different vault, the
- approved plan also requires permission to grant the Function identity secret-read access:
- role-assignment write for RBAC vaults, or access-policy write for access-policy vaults.
- The source vault must be in the customer tenant; `-KeyVaultSubscriptionId` identifies a
- different source subscription. Setup never fetches its private-key value locally.
+- **Azure Cloud Shell in PowerShell mode, or PowerShell 7+**, with an Azure CLI user login.
+ Certificate issuance stays in Key Vault; no local certificate-store cmdlets are needed.
+- Azure CLI **2.60.0+ for FC1** or **2.48.1+ for EP1** on `PATH`, with GitHub, Azure,
+ Microsoft Graph, Key Vault, and required SCM access. Setup can install Bicep after confirmation,
+ but Azure CLI must already be installed.
+- An Azure **user** allowed to deploy at subscription scope and create scoped role assignments.
+ Bicep grants **Key Vault Certificates Officer** and **Key Vault Secrets Officer** on this vault.
+- **Certificate reuse:** the operator needs source-vault ARM read, certificate read, and
+ secret-version metadata read. A different vault also requires permission to grant the Function
+ secret read through RBAC or an access policy. The vault must be in the customer tenant;
+ use `-KeyVaultSubscriptionId` for another subscription. Setup never reads the private key locally.
- A Microsoft Entra **Privileged Role Administrator** for granting the Microsoft first-party service
principal Graph `Application.Read.All`. Microsoft Graph operations use the current Azure CLI
user session through `az ad` and `az rest`; Azure subscription roles alone do not grant these
directory permissions.
-- Microsoft Graph **beta** access for the Entra `signInAudienceRestrictions` allowed-tenants preview.
- The selected provider tenant is allowed in addition to the app's home tenant, which Entra always allows.
+- Microsoft Graph **beta** access for `signInAudienceRestrictions`. The provider tenant is added;
+ Microsoft Entra ID always allows the home tenant.
See the [allowed-tenants schema](https://learn.microsoft.com/en-us/graph/api/resources/allowedtenantsaudience?view=graph-rest-beta).
-- The selected **Linux Flex Consumption FC1 or Premium EP1** plan available in the chosen region,
- with sufficient subscription quota. Availability is checked for that plan only; regional service
- availability and resource-provider registration do not guarantee quota. If deployment reports
- `SubscriptionIsOverQuotaForSku`, resolve the
- [regional quota issue](Troubleshooting.md#deployment-fails-with-subscriptionisoverquotaforsku)
- before retrying. Setup registers missing required Azure
- resource providers automatically after the single approval. The Azure account needs the
- providers' subscription-scoped `/register/action` permission (included in Contributor/Owner).
-- **.NET selection only:** install the .NET 8 SDK and allow NuGet access. Setup runs `dotnet publish`
- automatically for `linux-x64`, packages the publish output, and deploys it. No manual build step
- or upload is required. JavaScript and Python do not require this SDK.
-- **Python selection:** Azure performs the Linux dependency build. No local Python, pip, or Windows
- dependency installation is needed. The source archive is never used directly as run-from-package.
+- The selected **Linux FC1 or EP1** plan available in the region with enough quota. Resolve
+ [`SubscriptionIsOverQuotaForSku`](Troubleshooting.md#deployment-fails-with-subscriptionisoverquotaforsku)
+ before retrying. The operator needs subscription-scoped `/register/action`.
+- **.NET only:** install the .NET 8 SDK and allow NuGet access. Setup publishes for `linux-x64`,
+ packages, and deploys automatically.
+- **Python:** Azure builds Linux dependencies; no local Python/pip install is required. The source
+ archive is not a run-from-package artifact.
### Permissions are separate
-For an Azure built-in-role example, **Owner at the selected subscription** includes deployment,
-resource-provider registration, and role-assignment rights. **Contributor alone is insufficient**
-for `Microsoft.Authorization/roleAssignments/write`; Contributor plus **Role Based Access Control
-Administrator** at the required scope is another administrator-reviewed option, subject to any
-role-assignment conditions. Setup deploys at subscription scope and creates a new resource group;
-a role on an unrelated existing group is not sufficient. Request only approved scope/duration
-and activate eligible PIM roles before setup. See [Azure roles](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles).
+**Owner at the selected subscription** includes deployment, provider registration, and
+role-assignment rights. Another option is
+**Contributor** plus **Role Based Access Control Administrator** at the required scope, subject
+to any role-assignment conditions. Setup deploys at subscription scope and creates a new resource
+group, so access to an unrelated group does not cover the deployment. Activate eligible PIM roles before setup. See
+[Azure roles](https://learn.microsoft.com/azure/role-based-access-control/built-in-roles).
Azure roles do not grant Entra/Graph permissions, and Entra roles do not grant Azure/vault access.
-The Entra Privileged Role Administrator and directory permissions of the signed-in Azure CLI user
-above are for setup.
-The later Authentication Policy Administrator and policy permissions are a **separate activation**
-step. Setup grants vault data roles to its Azure operator and Function identity; it does not
-automatically grant them to every member of your operations team.
+The signed-in user needs the listed Entra roles for setup. An Authentication Policy Administrator
+handles activation separately. Setup grants vault roles only to its operator and Function identity.
### Install and check workstation tools
-Use the official [PowerShell installation](https://learn.microsoft.com/powershell/scripting/install/installing-powershell),
+Install missing tools using [PowerShell installation](https://learn.microsoft.com/powershell/scripting/install/installing-powershell),
[Azure CLI installation](https://learn.microsoft.com/cli/azure/install-azure-cli), and
[.NET 8 SDK download](https://dotnet.microsoft.com/download/dotnet/8.0) instructions (SDK only for C#).
Open a new **PowerShell 7** window after installation, then check:
@@ -185,8 +148,7 @@ az version
dotnet --list-sdks
```
-Missing Bicep can be installed by interactive setup after confirmation.
-These version checks do not establish Azure permissions, quota, feature availability, or provider access.
+Interactive setup can install missing Bicep.
| Choice | Azure runtime | Automatic deployment path |
|---|---|---|
@@ -194,26 +156,15 @@ These version checks do not establish Azure permissions, quota, feature availabi
| .NET | .NET 8 isolated, Functions v4 | Verify source ZIP, publish for Linux with .NET 8, repackage and publish |
| Python | Python 3.11, Functions v4 | Verify source ZIP; One Deploy with remote build on FC1, or validate/download and publish the SCM-built output on EP1 |
-Package hashes are still checked; removing the **customer prompt** does not disable integrity
-verification. Source and deployed-package hashes are recorded separately when a build changes the bytes.
-For FC1 Python, One Deploy builds and stores the output inside Azure; the summary records the
-verified source hash and leaves `packageSha256` null rather than reporting it as the built-output hash.
+Setup records source and built-package hashes separately. FC1 Python leaves `packageSha256` null
+because Azure builds the package.
-Setup normally detects these automatically. For noninteractive execution, preinstall prerequisites
-or explicitly allow Bicep installation with `-InstallPrerequisites`; see the
-[complete example](#noninteractive-example).
+For [unattended execution](#noninteractive-example), preinstall prerequisites or allow Bicep
+installation with `-InstallPrerequisites`. Azure CLI must already be installed. Sign-in, MFA,
+consent, and installation prompts are separate from deployment approval.
-Install Azure CLI through its official installation instructions if necessary. Setup starts by
-reading cached enabled user subscriptions without changing the CLI default. You can select a
-tenant and subscription, refresh the cache, or explicitly start device-code sign-in. Microsoft
-Graph uses the resulting CLI directory through `az ad` and `az rest`; there is no separate Graph
-PowerShell sign-in. Authentication, Bicep installation, MFA, and administrator-consent prompts
-are not resource-creation approvals.
-
-Use `-ForceAuthentication` when the machine has ambiguous cached identities. It requires interactive
-Azure CLI device-code authentication, does not clear shared token caches, and cannot be combined
-with `-NonInteractive`. Azure RBAC uses the selected ARM token's validated `oid`; Graph user
-readback comes from the same CLI directory session.
+Use `-ForceAuthentication` for ambiguous cached identities. It requests device-code authentication,
+does not clear shared caches, and cannot be combined with `-NonInteractive`.
## Step 2 - download and run one script
@@ -252,14 +203,10 @@ The flow is:
When `-Provider` is omitted, setup reads the application's allowed-tenants configuration:
no supported provider prompts for a name, one asks for confirmation, and several require a
name. Existing allowed provider tenants are preserved; provider options are never numbered.
- Setup downloads the provider JSON,
- resolves one complete regional route containing both SMS and voice endpoints, authentication, app-ID/scope
- when applicable, timeout, and retry interval. These values come from the selected profile;
- the launcher has no endpoint or credential override parameters. It also resolves the latest stable CI package
- release and looks up the language asset name in `packages/catalog.json`; there are no
- `PackageUrl` or `PackageSha256` inputs. Use
- `-PackageReleaseTag epp-packages--` to pin a previous CI release. Malformed or
- disabled profiles still fail before resource creation.
+ The selected [profile](#availability) supplies both channel URLs and auth/timing values.
+ The launcher has no endpoint/credential overrides or `PackageUrl`/`PackageSha256` inputs.
+ It uses `packages/catalog.json` and the latest stable CI release, or your
+ `-PackageReleaseTag epp-packages--`. Malformed/disabled profiles fail before resource creation.
5. **Service plan selection:** choose **Flex Consumption FC1** (free usage grant, zero always-ready
instances, both cache settings `"false"`) or **Premium EP1** (warm instance, both cache settings `"true"`).
Supply `-ServicePlan FC1` or `-ServicePlan EP1` to reuse a known selection.
@@ -349,26 +296,21 @@ at `Deploy this complete plan? Type Yes or No [No]`. Do not supply `-ApproveDepl
that switch authorizes the deployment phase without asking again.
This is a cancellation at the approval gate, not an offline simulation. The existing application
-registration is still required. Before the plan appears, setup can download tools and packages,
-request prerequisite installation, sign in through Azure CLI, perform permission and
-region checks, verify package checksums, build the C# package locally when selected, and compile Bicep.
+registration is still required. Before the plan appears, setup can download tools and packages, request required installation,
+and sign in through Azure CLI. It also checks permissions and regions, verifies package checksums,
+builds the C# package when selected, and compiles Bicep.
Installation, sign-in, and consent prompts are separate from deployment approval.
A preflight error is not a successful preview.
### After approving the plan
-After approval, setup rechecks the selected subscription and registers only missing
-`Microsoft.Web`, `Microsoft.Storage`, `Microsoft.KeyVault`, `Microsoft.OperationalInsights`,
-`Microsoft.Insights`, and `Microsoft.ManagedIdentity` providers. Already registered providers are
-left alone; existing registrations in progress are reused. Registration and regional checks happen
-before certificate creation or Bicep deployment. The read-only preflight does not register anything.
-
-Azure registers providers region by region. Setup does not unnecessarily wait for a global
-`Registered` state when a provider is already `Registering` and exposes the requested region.
-Registration metadata is polled with a bounded limit, and recognized regional registration
-propagation errors are retried during capability checks/deployment. Permission failures and
-unsupported regions remain explicit errors. Registration is subscription-wide and isn't undone
-automatically if a later deployment step fails.
+Setup rechecks the subscription and registers only missing `Microsoft.Web`, `Microsoft.Storage`,
+`Microsoft.KeyVault`, `Microsoft.OperationalInsights`, `Microsoft.Insights`, and
+`Microsoft.ManagedIdentity` providers before certificates or deployment. Preflight registers nothing.
+Existing registrations are reused; `Registering` is sufficient once the requested region is exposed.
+Polling/recognized propagation retries are bounded; permission and unsupported-region errors stop setup.
+Subscription-wide registrations are not undone after later failures. See
+[registration recovery](Troubleshooting.md#a-required-azure-resource-provider-is-not-registered).
Supply known values to shorten the prompts. Replace `` with your selected guided
provider's ID (`soprano` or `telesign`); neither is a default or recommendation:
@@ -393,21 +335,22 @@ The suffix does not include provider/region, so changing those inputs with the s
subscription/app/prefix can reconfigure the existing endpoint. See
[deployment separation and key constraints](../../docs/ONBOARDING.md#inputs-you-supply-to-setup).
-The plan creates or updates a dedicated resource group, selected Linux FC1 or EP1 hosting plan, Function App,
-storage account/private package container, Key Vault, Log Analytics workspace, Application Insights,
-outbound managed identity, diagnostics, Easy Auth, and scoped role assignments. Storage/package
-access uses managed identity, not account keys or Azure Storage shared access signatures.
+The plan creates or updates the complete deployment. This includes a dedicated resource group,
+Linux FC1 or EP1 hosting plan, Function App, storage, private package container, and Key Vault.
+It also includes Log Analytics, Application Insights, an outbound managed identity, diagnostics,
+Easy Auth, and scoped role assignments. Storage and package access use managed identity instead
+of account keys or Azure Storage shared access signatures.
Telemetry uses the system identity; the outbound identity is selected explicitly, not through
a global `AZURE_CLIENT_ID`.
The Function starts with public ingress disabled. After Bicep creates the vault and permissions,
-setup applies the approved certificate action. New certificates are self-signed, exportable
-RSA-2048 with subject and Entra display name **`CN=ExternalPhoneProvider`**. Initial creation uses
-`phone-provider-encryption`; replacement uses a separate suffixed staging name. Reuse selects the
-approved existing certificate version and preserves its registered key ID and display name.
-Setup pins `EPP_DECRYPTION_KEY_PEM` to the **versioned PEM backing secret**, verifies resolution,
-then writes exactly one `AsymmetricX509Cert` / `Encrypt` entry while preserving other credentials.
-It **reads back and verifies Easy Auth before enabling ingress**.
+setup applies the [approved certificate action](#encryption-certificate-selection).
+New certificates are self-signed, exportable RSA-2048 with subject/display name
+**`CN=ExternalPhoneProvider`**. Initial creation uses `phone-provider-encryption`; replacement uses
+a suffixed staging name; reuse preserves the selected key ID/display name.
+Setup pins `EPP_DECRYPTION_KEY_PEM` to its **versioned PEM backing secret**, verifies resolution,
+and registers one encryption entry without changing other credentials.
+It **verifies Easy Auth before enabling ingress**.
SCM basic authentication stays disabled for both plans. FC1 uses Entra-authenticated **One Deploy**
for every language, with remote build for Python. Its `functionAppConfig` defines runtime, scaling,
and managed-identity deployment storage; setup does not write Premium-only runtime/build or
@@ -418,38 +361,27 @@ It never mounts the unbuilt Python source ZIP. EP1 then restarts and synchronize
Both plans verify that `SendOtp` is registered before reporting success.
On publication/startup failure it disables public ingress again; failure to close ingress is reported
explicitly rather than hidden.
-Setup refreshes and checks the exact decryption-key reference through App Service with bounded
-retries for identity/role propagation. Failure to resolve it stops before changing registered
-encryption credentials; infrastructure and app settings may already have changed. This metadata
-check does not replace the required authorized evaluation of decryption before policy activation.
-
-The public certificate and a timestamped identifier
-summary are saved to `epp-output` beside the downloaded script, or to `-OutputDirectory`.
-The summary includes certificate/secret version identifiers, thumbprint, expiry, and manual renewal
-mode. Its service-plan and cache fields record configured settings, not verified runtime cache behavior.
-Setup never downloads, writes, or imports the private key locally: only the Function receives
-it through its managed-identity Key Vault reference. Certificate creation automatically supplies the
-backing secret; setup no longer writes a separate `phone-provider-decryption-key` secret.
-
-For unattended runs, supply every input including `-ServicePlan FC1` or `-ServicePlan EP1`,
-authenticate Azure CLI for both ARM and Microsoft Graph first, and explicitly authorize
-the whole displayed plan with **both** `-NonInteractive -ApproveDeployment`. `-NonInteractive`
-alone never approves changes. There is no `-Stage`, `-Resume`, `-ConfigPath`, or policy-approval switch.
-Existing encryption credentials also require explicit `-CertificateAction Reuse` or `Replace`.
-Unattended reuse requires both `-EncryptionKeyId` and `-KeyVaultCertificateId`, even if only one
-credential exists. `-ApproveDeployment` alone never selects a certificate or authorizes an
-implicit replacement.
+Key-reference checks use bounded identity/role propagation retries. Failure stops certificate
+registration, but infrastructure/settings may have changed. Metadata resolution does not replace
+authorized decryption evaluation before activation.
+
+Find the public certificate and timestamped [summary](#read-the-deployment-summary) in `epp-output`
+beside the script, or `-OutputDirectory`. Plan/cache fields record configuration, not verified
+runtime behavior. Setup never downloads, writes, or imports the private key locally: the Function
+receives it through its managed-identity reference. The certificate supplies its backing secret;
+there is no separate `phone-provider-decryption-key` secret.
### Noninteractive example
-This JavaScript deployment template supports both SMS and voice; it is not a credential script.
-Replace the three application/tenant/subscription IDs and `` (`soprano` or `telesign`),
-and choose the approved provider route, Azure region, prefix, and plan before running.
-The preliminary user sign-ins can require MFA and consent; `-NonInteractive` does not create a
-headless service-principal deployment path. Install Azure CLI Bicep beforehand using interactive
-setup's prerequisite prompt or your approved installation process.
-The `az login` step below is for local PowerShell. In Cloud Shell, start in the intended directory
-and reuse its existing login instead; token checks use the selected subscription, not `--tenant`.
+Supply every input, including `-ServicePlan`, and **both `-NonInteractive -ApproveDeployment`**
+to authorize the complete reviewed plan. `-NonInteractive` alone approves nothing; there is no
+`-Stage`, `-Resume`, `-ConfigPath`, or policy-approval switch.
+
+Replace the IDs/provider placeholder and choose approved route, region, prefix, and plan.
+This JavaScript example supports both channels. Establish the CLI user session for ARM and Graph
+first; MFA/consent may still be required. There is no headless service-principal path.
+Preinstall Bicep or explicitly allow its installation. In Cloud Shell, reuse the intended
+directory's login instead of the local `az login` below; token checks select the subscription.
```powershell
$tenantId = ''
@@ -487,8 +419,9 @@ mutation set, not just package upload. Do not use it as a dry run.
Add `-PassThru` when automation also needs the structured deployment result. The timestamped
summary JSON is saved regardless.
-The example above assumes the app has **no encryption credentials**. For a rerun, add
-`@reuseCertificate` to the complete setup command using this reviewed parameter set:
+The example assumes **no encryption credentials**. Otherwise choose `Reuse` or `Replace`;
+approval alone never selects or replaces a certificate. Reuse requires both key ID and exact
+certificate version, even with one credential. Add `@reuseCertificate` using this reviewed set:
```powershell
$reuseCertificate = @{
@@ -532,10 +465,9 @@ Those fields do not prove credentials, delivery, or monitoring work. Follow
### Encryption certificate lifecycle
-The policy for certificates **issued by setup** uses **12-month validity, key reuse, and manual renewal**. It specifies
-`EmailContacts` 30 days before expiry, **not `AutoRenew`**. Email is sent only if the customer
-separately configures Key Vault certificate contacts; setup does not create contacts or guarantee
-notifications. Track the saved expiry and arrange renewal before the certificate expires.
+Certificates **issued by setup** use **12-month validity, key reuse, and manual renewal**.
+`EmailContacts` runs 30 days before expiry, **not `AutoRenew`**, and needs separately configured
+vault contacts. Setup creates no contacts and guarantees no notifications.
In **Key Vault > Certificates > Certificate contacts**, add the approved operations/renewal
contact and verify the address and ownership. Portal labels can vary; follow
@@ -554,17 +486,16 @@ waits for self-signed issuance. Other errors, including pending-operation confli
rather than overwriting a disabled, incompatible, or near-expiry staged certificate. Keep more than
30 days of validity remaining. No Key Vault version is deleted as part of application cleanup.
-For renewal, coordinate with the EPP owner before expiry. **Replace** stages a new certificate
-and switches the single-key endpoint under the approved maintenance plan. Alternatively, use the
-supported administrator procedure to synchronize a renewed certificate in Entra and Key Vault,
-then select that exact registered version with **Reuse**. Rerunning setup no longer appends a
-renewed certificate alongside older credentials, even when it has the same RSA key.
+Coordinate renewal with the endpoint owner before expiry. **Replace** stages a new certificate
+and switches the single-key endpoint under approved maintenance. Alternatively, synchronize the
+renewed certificate in Microsoft Entra ID and Key Vault through the supported administrator
+procedure, then **Reuse** that exact registered version. Setup does not append a second encryption
+credential, even for the same RSA key.
-Key Vault renewal alone does **not** update Entra or the Function's pinned reference.
-Reuse does not change an external certificate's issuance/renewal policy. Even if that vault
-automatically issues a new version, endpoint synchronization remains manual; this is what the
-summary's `certificateRenewal: manual` records. The Function still decrypts in-process with a
-single private key, so any rotation must account for other endpoints and cached/in-flight requests.
+**Vault renewal alone updates neither the app registration nor the pinned Function reference.**
+Reuse leaves an external vault's issuance policy unchanged; automatic issuance there still
+requires manual endpoint synchronization (`certificateRenewal: manual`). Account for every
+endpoint sharing the key and cached/in-flight requests; decryption uses one in-process private key.
**Legacy deployments require an explicit decision.** A compatible matching Key Vault certificate
can be reused even if it has an older/custom subject. A standalone PEM secret without a Key Vault
@@ -580,23 +511,18 @@ follow [safe recovery](Troubleshooting.md#recover-without-destructive-cleanup).
### Source versioning
-**For developers testing unpublished changes:** publish to a public fork with a matching stable
-package release, then use `-SourceRepository ` and
-`-SourceRef `. Both must identify the same source as the downloaded
-launcher. Use `-PackageReleaseTag` if the fork contains more than one stable package release.
-Unpublished worktree changes are not downloadable from GitHub.
-
-`-SourceRepository` defaults to `Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample`.
-The small entry point resolves `-SourceRef` (default `main`) to a single commit in that repository. All supporting
-PowerShell, Bicep, both catalogs, and every catalog-listed provider profile are downloaded from that commit.
-The package catalog supplies asset names, while setup resolves the latest stable `epp-packages-*`
-release from the same repository and verifies the selected asset against that release's
-`SHA256SUMS.txt`. The plan and saved summary record the concrete versioned URL and hashes.
-For a fully repeatable deployment, use both a reviewed full commit SHA and
-`-PackageReleaseTag epp-packages--`. Provider JSON selects data only; it cannot
-redirect execution to another script. Download failures stop setup, and temporary downloads are
-removed on completion or failure. Select only a repository whose code you trust: its supporting
-PowerShell is executed locally.
+Defaults are `-SourceRepository Azure-Samples/ExternalPhoneProvider-AzureFunction-Sample` and
+`-SourceRef main`. Setup resolves one commit for supporting PowerShell, Bicep, both catalogs,
+and all catalog-listed profiles. The package catalog names assets from that repository's latest
+stable `epp-packages-*` release; `SHA256SUMS.txt` verifies them. The plan/summary records URLs and hashes.
+
+For repeatable deployment, pin a reviewed full SHA **and**
+`-PackageReleaseTag epp-packages--`. To test custom source, publish a public fork and
+matching stable release; align both source options with the downloaded launcher. Local unpushed
+changes are not downloadable.
+
+Trust the selected repository: its PowerShell executes locally. Provider JSON selects data, not
+another executable script. Downloads fail closed and temporary files are removed on success/failure.
### Offline setup checks
@@ -628,11 +554,11 @@ if ($LASTEXITCODE -ne 0) { throw 'ServicePlans.Tests.ps1 failed.' }
Remove-Item -LiteralPath $templatePath
```
-The suites cover commit-pinned launcher downloads and cleanup; cached account and subscription
-selection; Azure CLI Graph transport; application and region selection; provider discovery,
-allowlist preservation, and stale-plan rejection; certificate decisions and cleanup ordering;
-provider/package resolution; and FC1/EP1 deployment paths. GitHub, Azure, Microsoft Graph,
-certificate, and publication calls are mocked. CI runs the same eight suites on Windows and Linux.
+The suites cover launcher downloads and cleanup; account, subscription, application, and region
+selection; Azure CLI Graph calls; provider discovery; allowlist preservation; and stale-plan
+checks. They also cover certificate choices, provider and package selection, and FC1/EP1
+deployment. GitHub, Azure, Microsoft Graph, certificate, and publication calls are mocked.
+CI runs the same eight suites on Windows and Linux.
They do not certify live deployments, RBAC propagation,
Key Vault issuance, Entra behavior, or provider delivery.
@@ -653,9 +579,9 @@ configuration APIs alone do not establish EPP feature availability or an EPP upd
Do not invent a PATCH payload, assume a Graph success means activation, or use the general
Delete operation as an EPP rollback.
-Before a policy change, the **Authentication Policy Administrator** must obtain Microsoft's
-current approved EPP procedure for the selected tenant/channel: tool, API/version and exact
-field placement, required consent/roles, readback, validation, and rollback semantics.
+Before a policy change, the **Authentication Policy Administrator** gets Microsoft's current
+approved EPP procedure for the selected tenant and channel. It must identify the tool, API and
+version, exact field placement, required consent and roles, readback steps, validation, and rollback.
Delegated `Policy.ReadWrite.AuthenticationMethod` is a policy-management permission, not proof
of EPP entitlement; confirm the approved procedure's permission requirements separately from setup.
If this information or the feature is unavailable, leave policy unchanged and record the blocker.
diff --git a/setup/docs/Troubleshooting.md b/setup/docs/Troubleshooting.md
index 6be6c2b..6774973 100644
--- a/setup/docs/Troubleshooting.md
+++ b/setup/docs/Troubleshooting.md
@@ -1,19 +1,30 @@
# Setup and onboarding troubleshooting
-Find your symptom in the [triage table](#after-deployment-triage), then follow the matching checks.
-If the setup script stopped, start with [safe recovery](#recover-without-destructive-cleanup)
-before rerunning it. A successful deployment does not yet prove provider access or delivery.
+[Documentation](../../docs/README.md) | [Setup reference](README.md) | [Resume onboarding](../../docs/GETTING-STARTED.md)
-Return to the [main setup guide](../../README.md) when you are ready to continue onboarding.
+Select the failed stage and keep existing resources. Before rerunning, use
+[safe recovery](#recover-without-destructive-cleanup). Check provider outcomes before retrying timeouts.
+
+## Find your symptom
+
+| Stage / symptom | Start here |
+| --- | --- |
+| Account missing or wrong tenant | [Account picker](#the-expected-cached-account-or-subscription-is-missing), [directory context](#azure-cli-directory-context-does-not-match-the-selected-tenant) |
+| Permission or prerequisite failure | [Preflight](#authentication-permission-or-runtime-preflight-fails), [Graph 403](#az-ad-or-az-rest-returns-403-forbidden), [PrincipalNotFound](#principalnotfound-for-the-azure-operator) |
+| Region or hosting rejected | [Quota](#deployment-fails-with-subscriptionisoverquotaforsku), [plan selection](#service-plan-selection-or-availability-fails), [FC1 object-reference error](#fc1-deployment-preflight-reports-an-object-reference-error), [EP1 location check](#appservice-list-locations-rejects-ep1) |
+| Certificate or resource registration fails | [Certificate selection](#certificate-selection-reuse-or-cleanup-fails), [resource providers](#a-required-azure-resource-provider-is-not-registered) |
+| Download, build, or parser failure | [Old package prompts](#setup-still-asks-for-packageurl-or-packagesha256), [checksum/download](#a-checksum-or-package-download-fails), [.NET](#net-build-fails), [Python](#python-remote-build-fails), [CLI JSON](#azure-cli-warnings-break-json-parsing) |
+| Deployment stops after approval | [Publication and startup](#deployment-stops-after-approval) |
+| Endpoint rejects calls, does not deliver, or has no logs | [After-deployment triage](#after-deployment-triage), [401/live delivery](#the-endpoint-returns-401-or-live-delivery-fails), [cache settings](#credential-caching-does-not-match-the-selected-plan) |
+| Optional regional routing fails | [Front Door checks](#optional-front-door-deployments) |
## After-deployment triage
-Keep the timestamped deployment summary and original error. Record the intended tenant,
-subscription, resource group, runtime/release, test UTC time, and safe correlation IDs in a private
-support case. Never attach complete app settings, tokens, phone numbers, OTPs, private keys,
-encrypted request bodies, or raw provider responses. Do not enable body tracing for diagnosis.
+Keep the error and timestamped summary. For private support, record the tenant, subscription,
+resource group, runtime/release, UTC time, and safe correlation IDs. Exclude settings, tokens,
+phone numbers, OTPs, private keys, encrypted bodies, and raw responses. Keep body tracing off.
-| Symptom | First checks / owner | Do not do this |
+| Symptom | First checks / owner | Safeguard |
|---|---|---|
| Cannot see an offer or EPP activation/test capability | Customer onboarding owner verifies the [Security Store and feature-access gate](../../docs/ONBOARDING.md#before-purchasing-or-deploying) with provider/Microsoft support. | Do not infer eligibility from Azure Owner or a successful purchase. |
| Setup reports success, but no OTP delivery | Finish [Telesign vault entry or Soprano provider consent](../../docs/ONBOARDING.md#complete-provider-authentication); confirm authorized testing and policy activation are separately complete. | Do not redeploy all runtimes or paste credentials into local settings. |
@@ -26,35 +37,27 @@ encrypted request bodies, or raw provider responses. Do not enable body tracing
| Requests exist but Python/.NET event query is empty | Use [runtime-specific discovery](../../docs/MONITORING.md#runtime-specific-log-discovery). Python extras/.NET scopes may not be exported; .NET host OpenTelemetry does not initialize direct worker export. | Do not apply JavaScript's JSON parser to other runtimes or promise complete dependencies/exceptions. |
| No expiry email or alert | Operations owner checks vault certificate contacts/lifetime policy, independently configured reminders/action groups/rules, and notification tests. | Do not assume setup created alerts, or diagnostic settings deliver Key Vault expiry events. |
-An accepted provider response, HTTP 200, a matching evaluation nonce, and actual recipient
-delivery are different evidence. Retain only nonce-match booleans, statuses, latency, safe support
-IDs, and delivery-confirmation outcomes. [Run the complete acceptance checks](../../docs/ONBOARDING.md#validate-the-deployed-endpoint)
-before activation.
+Keep nonce-match **booleans**, statuses, latency, safe IDs, and delivery outcomes. Complete
+[acceptance checks](../../docs/ONBOARDING.md#validate-the-deployed-endpoint) before activation.
## Recover without destructive cleanup
-Before approval, setup's read-only checks can fail without Azure resource mutation (Bicep
-installation and sign-in may already have occurred). After approval, partial Azure/Entra changes
-can remain even if no success summary was written. Inspect the Azure subscription/resource-group
-deployment operations and the original failure before retrying.
-
-Correct the underlying error, then use the same reviewed tenant, subscription, app, runtime,
-plan, prefix, provider/route, and intended source/package versions for a recovery rerun.
-Changing identifiers can create new resources or break certificate continuity; changing a route
-can reconfigure the existing endpoint. Setup does not resume from a checkpoint or undo previous
-steps. For an active endpoint, arrange a change window because reruns can disable ingress and
-update app settings/Entra.
-
-Do not delete a resource group, purge/recreate the vault, remove encryption credentials, or revoke
-shared Microsoft service-principal grants as a generic retry strategy. Use the separate
-[rollback and decommissioning procedure](README.md#rollback-and-decommissioning) if retirement
-is actually intended.
+1. **Locate the failure.** Before approval, only sign-in or Bicep installation can change the
+ environment. After approval, partial Azure or Microsoft Entra ID changes can remain. Inspect
+ the error and **resource group > Deployments**.
+2. **Fix, then rerun with the same inputs:** tenant, subscription, app, runtime, plan, prefix,
+ provider/route, and source/package versions. Active endpoints need a change window; setup has
+ no checkpoint resume or automatic undo.
+3. **Verify the failed stage and complete acceptance checks.** Keep resource-group deletion,
+ vault purge/recreation, encryption-credential removal, and shared-grant revocation out of
+ retry steps. Use the separate [rollback procedure](README.md#rollback-and-decommissioning)
+ when retiring a deployment.
## Certificate selection, reuse, or cleanup fails
-Setup checks the application's encryption credentials before approval. With existing credentials,
-choose **Reuse** and a certificate key ID/location, or **Replace**; it never silently adds another
-encryption certificate on a rerun. See [certificate selection](README.md#encryption-certificate-selection).
+**Check:** compare the application key ID and exact vault version using
+[certificate selection](README.md#encryption-certificate-selection).
+Existing credentials require explicit **Reuse** or **Replace**, not an automatic new certificate.
| Failure | Safe recovery |
|---|---|
@@ -66,107 +69,81 @@ encryption certificate on a rerun. See [certificate selection](README.md#encrypt
| Credentials or the source vault's permission model changed after approval | Stop competing changes and rerun selection/approval against the current state. Do not force an outdated cleanup plan. |
| An encryption credential has an unsupported type, or a non-encryption credential cannot be read | Have the application owner review the credential and read permissions. Setup cannot safely preserve or interpret it and will not blindly reset the credential collection. |
-New certificates are prepared before old application entries are removed. A failed reference or
-source-vault access check leaves the registered encryption credentials intact, although Azure
-resources/app settings may already have changed. If registration succeeded and a later publication
-step failed, inspect the current app and choose **Reuse** with the certificate ID printed by setup;
-repeating **Replace** would request another rotation. Keep the same resource prefix for recovery.
-
-An approved cleanup removes only the listed `AsymmetricX509Cert` / `Encrypt` entries from the
-application. It never deletes vault certificates, keys, secrets, or unrelated application
-credentials. Coordinate maintenance and authorized evaluation for every active endpoint sharing
-the application; cached or in-flight requests can still use a retired key.
-
-## Optional Front Door deployments
-
-The single-region setup script does not provision Front Door or a multi-region readiness endpoint.
-Use the [manual Front Door guide](../../docs/FRONTDOOR.md) for that option.
-
-- A probe returning 401/403 or 404 is not healthy. Verify the dedicated readiness handler, its exact
- path, the origin host header, and the profile-pinned network restrictions. Do not exempt SendOtp
- or disable Easy Auth to make probes pass.
-- An authenticated SendOtp request returning 403 during an outage still failed. Check origin state,
- access restrictions, and authentication diagnostics; do not assume every 403 is a normal failover
- transition or that the caller retries it.
-- A successful request through the shared URL does not prove every origin works. Correlate safe
- request identifiers with regional telemetry and verify each origin after recovery.
+**Recover:** a failed key-reference check leaves registered encryption credentials intact, but
+resources or settings may have changed. If registration succeeded, choose **Reuse** with the
+printed certificate ID and same prefix.
+
+**Expected:** one approved encryption credential. Cleanup removes only approved
+`AsymmetricX509Cert` / `Encrypt` entries. Coordinate every endpoint sharing the app.
+
+
+## Multi-region Front Door deployments
+
+Follow the [Front Door guide](../../docs/FRONTDOOR.md), including readiness and origin checks.
+
+- A 401/403/404 probe is unhealthy. Check the readiness path, origin host header, and profile-pinned
+ network restriction. Never exempt SendOtp or disable Easy Auth.
+- An authenticated SendOtp 403 is a failure, not proof of failover. Check origin, ingress, and auth.
+- A shared-URL success does not prove every origin works; verify each with safe regional telemetry.
- Distinguish delayed aggregate metrics from event timestamps and client observations. See the
[recorded test results and limitations](../../docs/FRONTDOOR.md#observed-failover-results-and-limitations).
## Deployment fails with SubscriptionIsOverQuotaForSku
-This is a subscription quota check, not an authentication failure or a missing resource-provider
-registration. A region can support Linux Premium EP1 while your subscription has an **EP1 VMs**
-limit of zero there. Quotas are regional: an allowance in one region does not establish an
-allowance in another.
-
-1. Confirm the tenant, subscription, region, and SKU in the deployment plan and Azure error.
-2. In the Azure portal, open **Quotas**, select **App Service**, and filter to the intended
- subscription and region. Review **EP1 VMs**, its current usage, and the requested deployment's
- requirements. This is an App Service quota, not a general-purpose Compute VM quota.
-3. Request an increase sufficient for the deployment and any planned scaling. Requesting an
- increase does not mean it is approved; verify the effective limit after approval.
-4. If the quota is not adjustable in the portal or the request is rejected, create an Azure
- support request under **Service and subscription limits (quotas)** for
- **Function or Web App (Windows and Linux)**. Include the region, Linux deployment type, EP1
- SKU, current limit, requested limit, and the error's tracking ID.
+**Check:** the selected subscription's regional **EP1 VMs** quota, not authentication or resource
+registration. A supported Linux EP1 region can still have zero quota for your subscription.
+
+1. Confirm the tenant, subscription, region, and SKU in the plan and Azure error.
+2. In **Quotas > App Service**, filter to that subscription/region and inspect **EP1 VMs** usage
+ and limit. This is not a general Compute VM quota.
+3. Request enough quota for deployment and scaling, then verify the effective approved limit.
+4. If the portal cannot adjust it, open a **Service and subscription limits (quotas)** request for
+ **Function or Web App (Windows and Linux)**. Include region, Linux/EP1, current/requested limits,
+ and the tracking ID.
5. Alternatively, choose another region only after checking its quota, service availability,
and your residency and provider requirements. Review the updated deployment plan before approval.
-See the [Azure quotas overview](https://learn.microsoft.com/azure/quotas/quotas-overview) for the
-quota-management and support options. Setup does not request or guarantee a quota increase.
-Do not change the hosting SKU, disable Easy Auth, or change provider credentials to bypass this
-error. After resolving quota, rerun setup and complete the normal deployed validation checks.
+**Expected:** the approved quota is visible and sufficient. See
+[quota support](https://learn.microsoft.com/azure/quotas/quotas-overview). Rerun with the reviewed
+inputs after quota is available.
## Service plan selection or availability fails
-Choose `-ServicePlan FC1` for Flex Consumption or `-ServicePlan EP1` for Premium. The parameter is
-required in noninteractive mode. FC1 requires Azure CLI **2.60.0+**, while EP1 keeps **2.48.1+**.
-FC1 regional checks use `az functionapp list-flexconsumption-locations`; EP1 uses its existing ARM
-check below. Availability in one plan does not imply availability in the other.
+**Check:** `-ServicePlan FC1` needs Azure CLI **2.60.0+**; `EP1` needs **2.48.1+**.
+The parameter is required unattended. FC1 availability uses `az functionapp list-flexconsumption-locations`;
+EP1 uses the [ARM check below](#appservice-list-locations-rejects-ep1). Availability is plan-specific.
+FC1 has zero always-ready instances and a usage grant, not a zero-cost guarantee.
-FC1 offers a free usage grant, not a zero-cost guarantee. It configures zero always-ready instances;
-supporting services and usage beyond the grant can still incur charges.
-
-Setup refuses to switch an existing prefix between FC1 and EP1. Use a different prefix and validate
-the new endpoint before manually changing policy. For an active endpoint, also onboard a new
-dedicated application or coordinate key continuity. A new prefix still requires the existing
-application's certificate reuse/replacement decision; choose the existing Key Vault certificate
-to retain its key rather than inadvertently rotating every endpoint sharing that application.
-For older deployments without a service-plan tag,
-preflight checks the actual hosting-plan SKU. Use `-ServicePlan EP1` when rerunning an existing EP1
-deployment; do not delete tags to bypass the migration guard.
+**Fix:** follow [plan selection and migration rules](README.md#service-plan-selection). Use a
+different prefix and a dedicated app or approved key-continuity plan. A new prefix does not authorize key rotation.
+Existing EP1 deployments use `-ServicePlan EP1`; setup checks actual SKU when tags are absent.
+Never remove tags to bypass this guard.
## FC1 deployment preflight reports an object-reference error
-An affected setup template can fail with `InvalidTemplateDeployment` against
+**Symptom:** `InvalidTemplateDeployment` against
`Microsoft.Web/serverfarms` and the inner message `Object reference not set to an instance of an object`.
-The cause is the Function App's entire `properties` object being wrapped in a `union()` expression
-that contains the Flex deployment storage endpoint's runtime `reference()`. ARM defers that whole
-expression, including `serverFarmId`, so the provider cannot see the Function-to-plan association
-during preflight. This error is not evidence that the FC1 SKU or selected region is unsupported.
-
-The corrected Bicep keeps `properties` as a literal object with an explicit `serverFarmId` and makes
-only `functionAppConfig` conditional on FC1. The storage endpoint lookup, hosting-plan configuration,
-scaling, managed identities, authentication, and public-access settings are unchanged.
-
-Download the updated `Setup-Epp.ps1` from the intended source branch and rerun it with matching
-`-SourceRepository` and `-SourceRef ` values. A command pinned to an
-older commit still downloads the old support files and template. Keep the original tenant, subscription, application,
-language, service plan, and resource prefix rather than changing plans to bypass this error.
-`az deployment sub validate` can check the corrected template without creating resources. Passing
-validation does not prove successful publication, runtime startup, Easy Auth enforcement, or delivery.
+An older template wraps all Function `properties` in `union()` with a storage `reference()`.
+ARM defers `serverFarmId` with it, hiding the Function-to-plan association during preflight.
+This does not establish that FC1 or the region is unsupported.
+
+**Fix:** download the corrected launcher with matching `-SourceRepository` and
+`-SourceRef `. The fixed template keeps literal `properties` and
+explicit `serverFarmId`, with only `functionAppConfig` conditional; storage lookup, scaling,
+identity, authentication, and ingress behavior are unchanged. An old pinned commit still loads the old template.
+Keep the original deployment inputs rather than switching plans.
+
+**Verify:** `az deployment sub validate` checks the template without creating resources.
+It does not verify publication, startup, Easy Auth, or delivery.
## Credential caching does not match the selected plan
-Check `EPP_KEY_VAULT_CACHE_ENABLED` and `EPP_ACCESS_TOKEN_CACHE_ENABLED` on the serving Function App.
-Setup writes both as `"false"` for FC1 or `"true"` for EP1; rerunning setup restores those values.
-These settings only affect a deployed Function package that implements the cache readers.
-Deploy a supporting runtime release and restart after edits; older packages ignore the switches.
-Values must be `true` or `false` (case-insensitive, trimmed); unset defaults to enabled.
-Invalid selected values fail live credential acquisition closed without blocking evaluation.
-Only the selected provider's authentication mode chooses the switch; disabling it skips startup
-preparation and cross-request reuse, not live credential acquisition or the platform's decryption-key reference.
+**Check:** serving-app `EPP_KEY_VAULT_CACHE_ENABLED` and `EPP_ACCESS_TOKEN_CACHE_ENABLED`.
+Setup writes both as `"false"` for FC1 or `"true"` for EP1 and restores them on rerun.
+**Fix:** deploy a supporting release and restart after edits; older packages ignore the switches.
+The [cache contract](../../docs/CONTRACT.md#credential-caching-and-refresh) defines accepted values,
+fail-closed invalid settings, and runtime differences. Disabling a cache does not eliminate live
+credential acquisition or affect the platform's decryption-key reference.
## appservice list-locations rejects EP1
@@ -182,31 +159,23 @@ another Python runtime is not required to fix this check.
## A required Azure resource provider is not registered
-The current setup detects missing providers such as `Microsoft.Web` during read-only preflight
-and lists them in the resource plan instead of asking the customer to register them manually.
-After `Yes` (or explicit noninteractive approval), it registers only the six namespaces needed by
-this deployment in the supplied subscription. No registration occurs if approval is declined.
+Setup lists missing providers such as `Microsoft.Web` during preflight. After approval, it
+registers only the six required namespaces. Declining approval registers nothing.
-Already registered providers are skipped. `Registering` is not a failure: Azure registers each
-region separately, so setup proceeds when the needed region is exposed and retries recognized
-registration-propagation errors. Metadata polling is limited to 60 checks with 10-second pauses;
-regional propagation retries are limited to 12 attempts. An actively `Unregistering` provider is
-not reversed automatically.
+Registered providers are skipped. Setup proceeds from `Registering` when the region is exposed.
+Polling is limited to 60 checks at 10-second intervals and 12 regional propagation retries.
+It does not reverse `Unregistering`.
-If registration fails, inspect the original Azure CLI error. The account needs subscription-scoped
-resource-provider `/register/action` permission, generally included in Contributor or Owner.
-Setup cannot grant this permission or bypass a subscription policy. It stops before creating the
-certificate or deployment resources. Registrations already requested are left in place for a rerun;
-the script does not unregister services that other workloads might now use.
+If registration fails, inspect the Azure CLI error. The account needs subscription-scoped
+`/register/action`, generally included in Contributor or Owner. Setup leaves completed registration
+requests in place for the rerun.
## Azure CLI directory context does not match the selected tenant
-Setup selects an Azure subscription without changing the Azure CLI default tenant. Microsoft
-Graph operations use that CLI directory through `az ad` and `az rest`, so `az account show` must
-describe a public-cloud **user** session in the selected tenant.
-
-In interactive setup, choose **Refresh subscriptions from Azure** or **Sign in to Azure
-(device code)** from the account picker. You can also authenticate before rerunning:
+**Check:** `az account show` must identify an AzureCloud **user** session in the selected tenant.
+Setup does not change the CLI default tenant; Graph uses that directory through `az ad`/`az rest`.
+**Fix:** choose **Refresh subscriptions from Azure** or **Sign in to Azure (device code)**,
+or authenticate before rerunning:
```powershell
az login --tenant '' --use-device-code
@@ -221,10 +190,8 @@ after account selection. It does not run `az logout`, `az account clear`, or del
## The expected cached account or subscription is missing
-The account picker includes only enabled AzureCloud subscriptions associated with an Azure CLI
-**user** login. Disabled subscriptions, service-principal sessions, tenant-level entries, and
-other clouds are excluded. Refresh the CLI cache or choose device-code sign-in. A supplied
-subscription ID is never replaced with another subscription.
+The picker includes enabled AzureCloud subscriptions from an Azure CLI **user** login. Refresh
+the cache or use device-code sign-in. Setup never replaces a supplied subscription ID.
For unattended setup, establish the user session first and verify both the tenant and subscription:
@@ -240,32 +207,25 @@ interactive device-code flow.
## Setup still asks for PackageUrl or PackageSha256
-You are running an older launcher or source revision. Download `Setup-Epp.ps1` again and supply
-the intended `-SourceRepository` and `-SourceRef`. The current version asks for **one language**
-and reads its package URL and published checksum automatically. Remove old package URL/hash
-arguments from saved commands.
+**Fix:** refresh `Setup-Epp.ps1` and its reviewed `-SourceRepository`/`-SourceRef`; remove old
+URL/hash arguments. **Expected:** one language choice, with package URL/checksum read automatically.
## A checksum or package download fails
-Setup resolves the latest stable `epp-packages-*` CI release by default, then downloads the selected
-catalog asset and that release's `SHA256SUMS.txt`. Use `-PackageReleaseTag` when reproducing a
-specific release. The checksum file must contain exactly one valid entry for that asset. Missing,
-duplicate, malformed, or mismatched checksums fail closed; there is no manual-hash or
-skip-verification workaround. Verify the release assets and your access to GitHub.
+**Check:** GitHub access, the selected `epp-packages-*` release, asset, and `SHA256SUMS.txt`.
+Exactly one valid checksum entry must match; missing, duplicate, malformed, or mismatched entries
+fail closed. **Never bypass verification or supply a replacement hash.**
-Supporting tools, Bicep, catalogs, and provider JSON all come from the commit selected at startup.
-For a public-fork branch, pass both source options. A full commit SHA avoids branch-resolution
-API rate limits. Private repositories are not supported by these unauthenticated raw downloads.
+**Fix:** align the launcher and [source/package versions](README.md#source-versioning).
+Pin `-PackageReleaseTag` to reproduce a release. Public-fork branches need both source options;
+a full SHA avoids branch-resolution API rate limits. Private raw downloads are unsupported.
## .NET build fails
-Install the **.NET 8 SDK** and allow NuGet access. Setup selects an installed 8.x SDK, extracts the
-verified source into its temporary workspace, runs a Linux-targeted Release publish, checks the
-publish output, and creates the ready ZIP. The source ZIP is not uploaded as runnable code.
-Build failures occur before Azure resource creation and include the `dotnet` failure output.
-
-Do not manually replace the published source checksum with a hash of the build output. These
-represent different artifacts; setup computes the built artifact's hash itself.
+**Fix:** install the **.NET 8 SDK**, allow NuGet access, and inspect the reported `dotnet` error.
+**Expected:** setup selects 8.x, publishes verified source for Linux in its temporary workspace,
+checks output, and packages it before Azure resource creation. Source ZIPs are not runnable.
+Do not replace the source checksum with the built ZIP hash; setup calculates that separately.
## Python remote build fails
@@ -280,50 +240,32 @@ access to its SCM endpoint. Setup enables `SCM_DO_BUILD_DURING_DEPLOYMENT` and `
without `WEBSITE_RUN_FROM_PACKAGE` during the build, and requests Azure remote build explicitly.
It never installs Windows Python dependencies for the Linux app.
-SCM basic authentication remains disabled for both plans. The CLI uses Microsoft Entra authentication.
-For EP1, the built
-`site/wwwroot` snapshot must include the Python Functions dependency payload; an unbuilt source
-archive is rejected even when an upload command returned success. The built output is then stored
-in private Blob storage, and temporary remote-build settings are cleared.
+SCM basic authentication remains disabled. For EP1, `site/wwwroot` must contain the Python
+dependency payload; setup rejects unbuilt source, stores the built output in private Blob storage,
+and clears temporary build settings.
-If build, snapshot, publication, or startup fails after opening SCM ingress, setup attempts to
-disable public ingress again. An inability to close ingress is an explicit error requiring
-immediate administrator inspection. Do not bypass certificate errors or enable basic auth.
+After build or startup failure, setup closes public ingress. If that fails, inspect it immediately.
## Azure CLI warnings break JSON parsing
-The current helper separates stdout from stderr. Successful command JSON is parsed independently
-of SDK warnings, while stderr warnings are shown and nonzero exit codes still fail. Upgrade an
-older downloaded helper by refreshing the launcher/source revision.
+**Fix:** refresh the reviewed launcher/source revision.
+**Expected:** stdout JSON is parsed separately from visible stderr warnings; nonzero exits still fail.
## Authentication, permission, or runtime preflight fails
-Use Azure Cloud Shell in PowerShell mode, or PowerShell 7 with Azure CLI and Bicep. The Microsoft
-Graph PowerShell SDK is not used. Sign in to the customer tenant with a user account. ARM requests
-use the selected subscription; setup does not change the CLI default subscription or adopt
-unrelated resource groups. Directory requests reuse the same CLI session through `az ad` and
-`az rest`.
-
-Azure CLI itself must be installed before setup. Interactive setup can select cached user
-subscriptions, refresh them, or start tenant-specific device-code sign-in. Only the Azure CLI
-Bicep component can be installed after confirmation. In unattended runs, preinstall Bicep or pass
-`-InstallPrerequisites`, and establish the matching CLI user session before starting setup.
-
-Only the dedicated customer application registration must exist from manual Step 1. After approval,
-setup makes it multi-tenant, restricts it to its home tenant plus the provider JSON's `tenantId`
-and existing allowed provider tenants through the Entra allowed-tenants preview, creates both
-required service principals, adds and assigns
-`Epp.Invoke`, and grants the Microsoft phone-provider service principal Graph `Application.Read.All`.
-
-Azure subscription roles do not authorize Microsoft Graph. The signed-in CLI user must be able to
-read and update the application and service principals and create app-role assignments. Granting
-Microsoft Graph `Application.Read.All` to the Microsoft phone-provider service principal normally
-also requires an active Privileged Role Administrator role. Noninteractive runs must establish
-the CLI context first and supply `-ApproveDeployment` separately.
-
-The tenant restriction uses Microsoft Graph beta `signInAudienceRestrictions`. If that preview is
-unavailable or the tenant policy blocks it, setup stops before mutation rather than silently allowing
-all organizational tenants.
+1. Check [tools and permissions](README.md#prerequisites-for-step-2): PowerShell 7/Cloud Shell,
+ installed Azure CLI, and a customer-tenant **user** session. Graph reuses that session; no Graph
+ PowerShell SDK. ARM uses the selected subscription without adopting unrelated resource groups
+ or changing the CLI default subscription.
+2. Resolve missing Bicep through its interactive confirmation, or preinstall/use
+ `-InstallPrerequisites` unattended. Azure CLI itself is not installed by setup.
+3. Verify the [dedicated app and approved grants](README.md#step-1---manually-create-the-application).
+ Azure roles do not grant Graph permissions; the user needs application/service-principal
+ management and app-role assignment permissions, including active Privileged Role Administrator
+ for the approved `Application.Read.All` grant.
+4. Confirm beta `signInAudienceRestrictions` availability. If tenant policy/preview access blocks it,
+ stop; setup does not silently allow all tenants. Unattended runs still need the user session
+ and separate `-ApproveDeployment`.
## `az ad` or `az rest` returns 403 Forbidden
@@ -339,11 +281,9 @@ az rest --method get `
--output json
```
-A 403 is a directory authorization, Conditional Access, or role-activation problem; Azure
-subscription Owner/Contributor does not fix it. Have the Entra administrator verify the user's
-effective application-management permissions and activate Privileged Role Administrator when
-the approved app-role grant requires it. Then rerun with the same reviewed inputs. Do not install
-or connect the Graph PowerShell SDK as a workaround, and do not paste access tokens into tickets.
+A 403 indicates directory authorization, Conditional Access, or role activation. Azure
+Owner/Contributor does not grant this access. Verify application-management permissions and
+Privileged Role Administrator activation, then rerun. Do not paste tokens into tickets.
The Graph user ID is tracked for directory operations only. Azure role assignments continue to
use the validated `oid` from the selected subscription's ARM access token.
@@ -366,9 +306,9 @@ app to another runtime with the same prefix.
## Deployment stops after approval
-Some resources can remain. No automatic deletion, vault purge/recovery, policy activation, or
-rollback occurs. Inspect the named Azure deployment and the reported error, then rerun with the
-same tenant, subscription, application, language, service plan, and prefix after correcting it.
+**Check:** the named deployment and original error. Partial resources remain; setup does not
+delete, purge/recover vaults, activate policy, or roll back. Follow
+[safe recovery](#recover-without-destructive-cleanup) before a same-input rerun.
Recognized storage/Key Vault RBAC propagation errors are retried for at most twelve attempts.
Transient Function startup errors also have bounded retries. This includes the specific ARM
@@ -378,23 +318,23 @@ return while a newly restarted host is still loading an otherwise valid package.
`SendOtp` must appear in Azure's function metadata. No success summary is written if publication or
registration fails.
-If setup exhausts the retries, inspect Application Insights for host initialization, worker startup,
-and function discovery errors before rerunning. The expected healthy sequence includes `Worker process
-started and initialized`, `Found the following functions: Host.Functions.SendOtp`, and `Job host
-started`. Setup closes public ingress after a persistent publication failure.
+**Verify:** inspect [host/worker logs](../../docs/APPLICATION-INSIGHTS.md#single-region-configuration)
+for startup/discovery failures. Expected messages include `Worker process started and initialized`,
+`Found the following functions: Host.Functions.SendOtp`, and `Job host started`.
+Setup closes public ingress after persistent publication failure.
## The endpoint returns 401 or live delivery fails
-Keep Easy Auth enabled. Check the trusted tenant, actual token version, audience, HTTPS requirement,
-and nonempty Microsoft caller allowlist. Keep `tokenEncryptionKeyId` null on the endpoint app;
-payload JWE encryption is separate from signed bearer-token validation.
-
-For live delivery, confirm the request's channel-specific provider URL
-(`EPP_PROVIDER_SMS_ENDPOINT` or `EPP_PROVIDER_VOICE_ENDPOINT`) and its account authorization.
-The matching setting is required; missing, empty, or invalid URLs fail closed without using a
-shared or other-channel endpoint. Use a package that supports these settings.
-Telesign needs its exact Key Vault secret names; Soprano needs provider-side authorization, not
-API-key secrets. Arrange an authorized evaluation through the
-[test handoff](../../docs/ONBOARDING.md#validate-the-deployed-endpoint) before controlled live
-messages. EPP policy remains a separate assisted administrator-approved operation; no setup code
-updates it. Public Graph SMS/voice schemas do not document the EPP activation fields.
+1. **Before invocation:** use the [Easy Auth checklist](../../docs/ONBOARDING.md#manual-deployment-trust-checklist).
+ Check issuer, token version, audience, HTTPS, and the nonempty Microsoft caller allowlist.
+ Keep Easy Auth enabled and app `tokenEncryptionKeyId` null; bearer validation and payload JWE are separate.
+2. **After invocation:** correlate [runtime events](../../docs/MONITORING.md#runtime-specific-log-discovery).
+ Confirm `EPP_PROVIDER_SMS_ENDPOINT` or `EPP_PROVIDER_VOICE_ENDPOINT` and account access.
+ The selected URL is required and has no shared/other-channel fallback; deploy a supporting package.
+3. Complete [provider authentication](../../docs/ONBOARDING.md#complete-provider-authentication):
+ exact vault secrets for Telesign, provider-side authorization for Soprano.
+ Arrange [authorized evaluation, then controlled live checks](../../docs/ONBOARDING.md#validate-the-deployed-endpoint).
+ Verify acceptance and receipt separately; never blindly resend a timeout.
+
+**Activation remains separate:** use the [assisted policy procedure](README.md#step-3---manually-validate-and-activate-policy).
+Setup does not change policy, and public Graph SMS/voice schemas do not document the EPP fields.