Skip to content

Implement TenantResolver abstraction in apps/api #18

Description

@patoperpetua

Parent: #2
Depends on: #17
Blocks: #16 (send endpoint)

Goal

Add a TenantResolver interface to apps/api so tenant identity is always derived server-side from authenticated credentials — never from a caller-supplied payload field. The initial implementation resolves tenants from API keys stored in Azure App Configuration / Key Vault.

Background

The current apps/api contact endpoint uses Origin-based host-profile routing as a Phase 1 routing hint (see contact.ts comments). The template-send epic (#16) needs a proper tenant authentication layer where the resolved TenantContext (tenantId, environment) is authoritative and cannot be spoofed by the request body.

Scope

Interface (in apps/api/src/tenant/)

import type { TenantContext } from '@singleton-sd/post-kit-types';

interface TenantResolver {
  resolve(request: HttpRequest): Promise<TenantContext>;
}
  • resolve throws (or returns a typed error) with PostKitErrorCode.UNAUTHENTICATED if no credential is present.
  • resolve throws with PostKitErrorCode.UNAUTHORIZED if the credential is present but maps to no known tenant.

API-key implementation

  • Extract the bearer token from Authorization: Bearer <token> header (or a configurable header).
  • Look up the token in the tenant key map loaded from Azure App Configuration (use the existing app-configuration.ts pattern in apps/api/src/config/).
  • Map token → { tenantId, environment }.
  • Never log or echo the raw token.
  • Support multiple keys per tenant (e.g. rotating credentials) — the map value is an array or a lookup table.

Configuration

  • Tenant API key map loaded via Azure App Configuration reference (already wired in src/config/app-configuration.ts — read that file before implementing).
  • Local dev: .env / local.settings.json — add example keys to .env.example (no real values).

Error model

Use PostKitErrorCode from @singleton-sd/post-kit-types. Return structured HTTP error responses consistent with the existing error handling in apps/api.

Constraints

  • Tenant identity must be fully server-side. The request body must not contain a tenantId field that is trusted directly.
  • Do not log API keys or bearer tokens anywhere.
  • The resolver must be testable without a live Azure connection (inject config as a dependency).
  • Follow the module/file conventions in apps/api/src/config/ and apps/api/src/.
  • Do not change the existing /contact endpoint behaviour — this abstraction is for the new template-send endpoint only.

Acceptance criteria

  • TenantResolver interface is defined and exported from apps/api.
  • API-key implementation resolves TenantContext from Authorization header.
  • Unauthenticated requests produce UNAUTHENTICATED error; unknown key produces UNAUTHORIZED.
  • Tenant key map is loaded from App Configuration — not hard-coded.
  • .env.example is updated with the new key-map config variable (no real values).
  • Unit tests cover: valid key resolves correct tenant, missing header, unknown key, malformed header.
  • pnpm -r --if-present run test passes.
  • No API keys appear in logs, responses, or test fixtures.

Agent implementation notes

Read apps/api/src/config/app-configuration.ts before implementing to match the existing config loading pattern. Read apps/api/src/contact.ts to understand the current request-handling style. Start the branch from origin/main: feat/18-tenant-resolver (use the actual GitHub issue number).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-readyMeets every criterion in docs/github-source-of-truth.md, section 4 — safe for an agent to claimenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions