Use when editing apps/azure/ auth, storage, or cloud sync code. EasyAuth (no MSAL code), IndexedDB via Dexie schema, Blob Storage sync with SAS tokens, /api/storage-token endpoint, App Insights telemetry with strict no-PII rule, customer-owned data principle (ADR-059), Azure feature stores in features/*/.
Use when editing any of the following in apps/azure/:
src/auth/easyAuth.ts, src/hooks/useAdminAccess.ts)src/db/schema.ts, src/services/localDb.ts)src/services/cloudSync.ts, src/services/blobClient.ts)src/services/storage.ts)src/lib/appInsights.ts)src/components/FileBrowseButton.tsx)server.js: /api/storage-token, /api/kb-upload, /api/kb-search)App Service Authentication (EasyAuth) handles all auth at the platform level. The client application contains .
Flow:
/.auth/login/aad → Entra ID login.x-ms-client-principal into every server request.src/auth/easyAuth.ts fetch /.auth/me to get the current user identity and access token.Key endpoints (built into every App Service — no application code required):
| Endpoint | Purpose |
|---|---|
/.auth/login/aad | Redirect to Entra ID sign-in |
/.auth/logout | Sign out, clear session |
/.auth/me | Current user info + access tokens |
/.auth/refresh | Refresh session token |
Client helper API (src/auth/easyAuth.ts):
getEasyAuthUser() — returns { name, email, userId, roles } from /.auth/megetAccessToken() — returns access token; calls /.auth/refresh proactively if token expires within 5 minutesstartPeriodicRefresh() / stopPeriodicRefresh() — 45-minute background refresh to prevent session expiry during long sessionslogin() / logout() — redirect helperslocalhost: helper detects local dev and returns mock user; getAccessToken() throws AuthError('local_dev')Server-side auth check (in server.js): presence of x-ms-client-principal header. The LOCAL_DEV bypass is blocked on deployed App Service via WEBSITE_SITE_NAME env var.
Required permissions — both tiers require zero admin consent:
User.Read (delegated) — user profilePeople.Read (delegated, Team only) — people pickerDo not add Graph API scopes. All prior Graph API scopes (Files.ReadWrite.All, etc.) were removed in ADR-059.
The Dexie database (VaRiScoutAzure) is defined in apps/azure/src/db/schema.ts.
Schema tables:
| Table | Key | Purpose |
|---|---|---|
projects | name | Full project data + lightweight meta (ProjectMetadata) |
syncQueue | ++id | Offline queue for pending cloud sync |
syncState | name | Cloud sync state: cloudId, etag, lastSynced, baseStateJson |
photoQueue | ++id | Offline queue for pending photo uploads |
channelDriveCache | channelId | Cached channel drive info (Teams-era remnant, retained for schema compatibility) |
Current version: 3. Always add new tables as a new version(N).stores({...}) block — never modify an existing version.
ProjectRecord shape:
interface ProjectRecord {
name: string;
location: 'team' | 'personal';
modified: Date;
synced: boolean;
data: unknown; // opaque project JSON
meta?: ProjectMetadata;
}
Service facade: apps/azure/src/services/localDb.ts
saveToIndexedDB(project, name, location, meta?) — upsert via db.projects.put()loadFromIndexedDB(name) — fetch by name, return data fieldlistFromIndexedDB() — return all records as CloudProject[]markAsSynced(name, cloudId, etag, baseStateJson?) — update syncState tableextractMetadataInputs(project, userId, existingLastViewedAt?) — peek into opaque project to build ProjectMetadataTeam plan only (hasTeamFeatures() guard in storage.ts). Data stays in the customer's own Azure tenant — VariScout's server never reads or stores the data.
SAS token flow:
Browser → POST /api/storage-token
App Service validates EasyAuth session (x-ms-client-principal)
App Service managed identity generates container-scoped SAS token
Returns: { sasUrl, expiresOn }
Browser → Azure Blob Storage (direct PUT/GET using SAS URL)
SAS token properties:
variscout-projects container only/api/storage-token again when expiredBlob structure under variscout-projects container:
{projectId}/
analysis.json — full project data
metadata.json — name, owner, updated, phase
knowledge-index.json — Foundry IQ knowledge index (Team + AI)
photos/{findingId}/{photoId}.jpg
documents/ — uploaded SOPs, specs, FMEAs
investigation/ — findings/questions JSONL
_index.json — project listing cache
cloudSync.ts wraps blobClient.ts operations:
saveToCloud(token, project, name, location) — write analysis.json + metadata.json; fire-and-forget _index.json updateloadFromCloud(token, name, location) — read via stored cloudId in syncStatelistFromCloud(token, location) — read from _index.jsongetCloudModifiedDate(token, name, location) — read metadata.json for conflict detectionCloudSyncUnavailableError — thrown when /api/storage-token returns 503 (not configured); storage.ts degrades gracefully to local-onlyConflict resolution: ETag-based. syncState.baseStateJson stores the last-loaded cloud snapshot as three-way merge base. On save, if cloud has changed since last load, merge.ts performs structural merge; conflicting versions are saved with (conflict copy) suffix.
apps/azure/src/services/storage.ts is a React Context (StorageProvider) that orchestrates localDb.ts and cloudSync.ts.
Save strategy (offline-first):
syncQueue; sync on reconnect.saveToCloud; exponential backoff retry on failure (delays: 2s, 4s, 8s, 16s, 32s; max 5 retries).Load strategy:
loadFromIndexedDB only.loadFromCloud (cache result in IndexedDB); fallback to local on error.useStorage() hook exposes: saveProject, loadProject, listProjects, syncStatus, notifications, dismissNotification.
File: apps/azure/src/lib/appInsights.ts
Initialize once at startup after loading runtime config:
await initAppInsights({ connectionString, plan });
Connection string comes from /config endpoint (env var APPINSIGHTS_CONNECTION_STRING). No-ops if empty (local dev, tests).
What is tracked:
enableAutoRouteTracking)AI.Call events — feature name, model, duration ms, token counts, success flagAI.Summary events — aggregate counts, success rate, p95 durationtrackException(error)deployment.plan (standard/team)What is never tracked:
Telemetry goes to the customer's own App Insights instance. Fixed-rate sampling at 80% to prevent quota exhaustion.
AI traces flush periodically (every 5 minutes) and on beforeunload. Use teardownTelemetry() in tests to prevent state leakage.
File: apps/azure/src/components/FileBrowseButton.tsx
SharePoint/OneDrive file picker was removed per ADR-059. FileBrowseButton now wraps a native <input type="file">.
Usage:
<FileBrowseButton
mode="files"
filters={['.xlsx', '.csv']}
onLocalFile={(file) => handleFile(file)}
onPick={() => {}} // no-op; SharePoint picker removed
/>
The FilePickerResult interface and onPick prop are retained for API compatibility but the SharePoint flow is not implemented. Always use onLocalFile for actual file handling.
Do not re-introduce any SharePoint/OneDrive SDK calls. ADR-059 explicitly removed this dependency.
Rolling your own auth instead of EasyAuth. Do not add MSAL, OAuth, or token exchange logic in client code. EasyAuth handles the full auth flow at the platform level. Adding client-side auth will conflict with the cookie-based session and break the auth flow.
Logging identity fields to App Insights. Never include name, email, userId, or similar fields in trackEvent / trackException calls. These are PII. Log structural metadata only: counts, types, durations, success/failure flags. The trackAICall function deliberately sends error type (hasError) rather than the full error message.
Using a SAS token after expiry. SAS tokens expire after 1 hour. Blob Storage operations will return 403. On expiry, call POST /api/storage-token to mint a fresh token. The blobClient.ts module handles token refresh internally — do not cache and reuse SAS URLs beyond a single session.
Introducing server-side data processing. server.js is a static file server plus thin token-minting proxy. It must not read, transform, or store project data. All analysis runs in the browser (ADR-059 browser-only principle). The KB upload endpoint streams files directly to Blob Storage without inspecting content.
Writing MSAL client code. There is no MSAL dependency in apps/azure. EasyAuth cookie flow is the only auth mechanism. If you need an access token for the Cognitive Services API, call getAccessToken() from src/auth/easyAuth.ts — it reads from /.auth/me, no MSAL required.
apps/azure/server.js (/api/storage-token, /api/kb-upload, /api/kb-search, /api/kb-list, /api/kb-delete, /api/kb-download)apps/azure/src/db/schema.tsapps/azure/src/services/localDb.ts, cloudSync.ts, blobClient.ts, storage.tsapps/azure/src/auth/easyAuth.tsapps/azure/src/lib/appInsights.ts