> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deploybaseline.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Microsoft

> Entra ID OAuth 2.0 and OIDC, Microsoft Graph: Outlook mail, calendar, contacts, To Do, users and groups, and SharePoint Online and OneDrive over Graph and the SharePoint REST API.

Microsoft Entra ID (Azure AD) v2.0 OAuth 2.0 and OpenID Connect emulation with authorization code flow, PKCE, client credentials, client-bound refresh tokens, RS256 ID tokens, OIDC discovery, and a stateful Outlook emulator on Microsoft Graph v1.0 and beta: the user directory, mail, calendar, contacts, Microsoft To Do, rooms and places, Microsoft 365 groups, and change notifications. SharePoint Online and OneDrive are served through Graph and the SharePoint REST API from one shared state (see [SharePoint and OneDrive](#sharepoint-and-onedrive)).

## At a glance

| | |
| - | - |
| Package | `@deploybaseline/microsoft` |
| Port | `4005` in the default set, `4000` when started alone |
| Config key | `microsoft` |
| HTTP operations | 544, browsable at `http://localhost:3999/docs/microsoft` while it runs ([API reference](/api-reference.md)) |
| Vendor spec | [microsoftgraph/msgraph-metadata](https://github.com/microsoftgraph/msgraph-metadata) at `1fd0ef5` |

```bash CLI theme={null}
npx @deploybaseline/cli start --service microsoft
```

```ts Programmatic theme={null}
import { createEmulator } from "@deploybaseline/cli";

const microsoft = await createEmulator({ service: "microsoft" });
// microsoft.url is http://localhost:4000
```

```json baseline.config.json theme={null}
{
  "microsoft": {
    "users": [
      {
        "email": "testuser@outlook.com",
        "name": "Test User"
      }
    ],
    "oauth_clients": [
      {
        "client_id": "example-client-id",
        "client_secret": "example-client-secret",
        "name": "My Microsoft App",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/microsoft-entra-id"]
      }
    ]
  }
}
```

## Start

```bash theme={null}
npx @deploybaseline/cli start --service microsoft
```

Microsoft listens on port `4005` in the default set, or on `http://localhost:4000` when started alone. Sign-in and Graph v1.0 and beta share the port. Without a seed config there is one user (`testuser@outlook.com`, Test User) in tenant `9188040d-6c67-4c5b-b112-36a304b66dad`.

### Using the Graph SDK

`@microsoft/microsoft-graph-client` attaches the token and follows `@odata.nextLink` only for `https` hosts. Start the emulator with an `https` base URL (it still listens on plain HTTP), list the host in `customHosts`, and end the middleware chain with a transport that sends each request to the HTTP listener:

```bash theme={null}
npx @deploybaseline/cli --service microsoft --port 4005 --base-url https://localhost:4005
```

```typescript theme={null}
import { AuthenticationHandler, Client, RetryHandler, type Context } from '@microsoft/microsoft-graph-client'

const transport = {
  async execute(context: Context) {
    const url = typeof context.request === 'string' ? context.request : context.request.url
    context.response = await fetch(url.replace(/^https:/, 'http:'), context.options)
  },
}

const client = Client.initWithMiddleware({
  baseUrl: 'https://localhost:4005',
  customHosts: new Set(['localhost']),
  middleware: [new AuthenticationHandler({ getAccessToken: async () => accessToken }), new RetryHandler(), transport],
})

const me = await client.api('/me').get()
await client.api('/me/sendMail').post({
  message: {
    subject: 'Hello',
    body: { contentType: 'Text', content: 'Hi Bob' },
    toRecipients: [{ emailAddress: { address: 'bob@example.com' } }],
  },
})
```

Instead of the transport, you can also put any TLS reverse proxy in front of the emulator and pass its URL as `--base-url`.

Plain `fetch` or curl against `http://localhost:4005/v1.0/...` needs none of this. MSAL Node takes the emulator as its authority (`<base>/<tenant_id>` with the host in `knownAuthorities`) and a custom `system.networkClient` that sends its requests to the HTTP listener; [`src/__tests__/graph-sdk.ts`](https://github.com/deploybaseline/cli/blob/main/packages/@deploybaseline/microsoft/src/__tests__/graph-sdk.ts) has a complete one. The Kiota based `@microsoft/msgraph-sdk` works with `@azure/identity` given `authorityHost` (the emulator) and `disableInstanceDiscovery: true` on the credential, with `adapter.baseUrl` set to `<base>/v1.0`.

The package tests (`graph-mail.sdk.test.ts`, `graph-users.sdk.test.ts`, and `graph-kiota.sdk.test.ts`) run all three libraries against the emulator unchanged: client credentials and authorization code flows, a draft with an attachment, send, reply, delta, `PageIterator`, `$batch`, errors and MIME, and with the Kiota SDK a meeting invite and acceptance, `calendarView` with `Prefer: outlook.timezone`, `getSchedule`, contacts with a lambda filter, and a cancellation. The conformance scenarios in `conformance/scenarios/sdk.ts` go further: the Kiota SDK and the Graph JavaScript client run a mail workflow against a real Microsoft 365 test tenant, their traffic is recorded, and the emulator must reproduce every response.

## Auth

* Access tokens are RS256 JWTs from this emulator's token endpoint. Authorization code and refresh tokens are delegated: their `scp` holds every Graph scope the user has consented to for the client, as in Entra ID, and `https://graph.microsoft.com/.default` grants the client's `delegated_permissions`. Client credentials tokens are application tokens: they need a `/.default` scope (anything else is `AADSTS1002012 invalid_scope`) and carry `roles` from the client's `application_permissions`. A client without either list gets every modeled permission.
* Each operation checks the least-privileged Graph permission, `scp` for delegated tokens and `roles` for application tokens, with Graph's error for each surface (`403 Authorization_RequestDenied` on the directory, `403 ErrorAccessDenied` on Exchange). `/me` with an application token is `400 BadRequest` (`/me request is only valid with delegated authentication flow.`).
* A missing, expired, revoked, or foreign token is `401 InvalidAuthenticationToken` with a `WWW-Authenticate` header. A token issued for another resource is `Access token validation failure. Invalid audience.`
* Without `graph.strict_auth: true`, an opaque bearer token the emulator did not issue (any string such as `Bearer test`) acts as the first seeded user with full access, so quick scripts work without an OAuth flow. JWTs are always verified, and a request without a token is always `401`.
* A scope for another resource, such as `https://contoso.sharepoint.com/.default`, gives a token for that resource. SharePoint tokens carry `aud: 00000003-0000-0ff1-ce00-000000000000` (the SharePoint app id), whether the scope names a tenant host or that id, as Entra issues them. Graph refuses them, and the SharePoint REST API takes only them.
* Delegated directory writes (`POST /users`, `PATCH` or `DELETE /users/{id}` on someone else) need the permission only. The emulator does not model Entra admin roles.

## OAuth and OIDC

* `GET /.well-known/openid-configuration` — Get OIDC discovery document: for the root issuer
* `GET /:tenant/v2.0/.well-known/openid-configuration` — Get tenant OIDC discovery document
* `GET /discovery/v2.0/keys` — Get JSON Web Key Set (JWKS)
* `GET /:tenant/discovery/v2.0/keys` — Get tenant JSON Web Key Set (JWKS)
* `GET /oauth2/v2.0/authorize` — Authorize: shows the user picker
* `GET /:tenant/oauth2/v2.0/authorize` — Authorize in tenant: shows the user picker
* `POST /oauth2/v2.0/token` — Exchange token: authorization code, refresh token, and client credentials grants; `client_secret_post`, `client_secret_basic`, or `private_key_jwt` (a certificate assertion)
* `POST /:tenant/oauth2/v2.0/token` — Exchange token in tenant: the same grants and client authentication
* `GET /oidc/userinfo` — Get user info: OpenID Connect
* `GET /oauth2/v2.0/logout` — End session: logout
* `GET /:tenant/oauth2/v2.0/logout` — End session in tenant: logout
* `POST /oauth2/v2.0/revoke` — Revoke token

### Authorization code flow

1. Redirect the user to `/oauth2/v2.0/authorize` with `client_id`, `redirect_uri`, `scope`, `state`, and optionally `nonce`, `response_mode`, `code_challenge`, and `code_challenge_method`
2. The emulator renders a user picker page
3. On selection, the emulator redirects to `redirect_uri` with `code` and `state` (in the query, after `#` for `response_mode=fragment`, or as an auto-submitted form for `form_post`)
4. Exchange the code for tokens via `POST /oauth2/v2.0/token`

### Issuers

Each discovery document is its own issuer, and the tokens minted through its endpoints carry it:

* `/{tenant}/v2.0/.well-known/openid-configuration` (as on Entra ID) has issuer `<base>/<tenant>/v2.0` and tenant-scoped endpoints (`/{tenant}/oauth2/v2.0/authorize`, `/{tenant}/oauth2/v2.0/token`, `/{tenant}/discovery/v2.0/keys`, `/{tenant}/oauth2/v2.0/logout`). ID tokens from these endpoints have `iss` `<base>/<user's tenant>/v2.0`, also through `common`, `organizations`, and `consumers`, whose documents name the default tenant.
* `/.well-known/openid-configuration` (an emulator convenience; Entra ID has no root document) has issuer `<base>` and the root endpoints (`/oauth2/v2.0/authorize`, and so on). ID tokens from a sign-in that started at the root authorize endpoint have `iss` `<base>`.

So set your client's issuer to either `<base>/<tenant>/v2.0` (closest to production, where it is `https://login.microsoftonline.com/<tenant>/v2.0`) or `<base>`, and let it discover the endpoints. Hard-coding the root endpoints together with a tenant issuer fails the `iss` check.

### PKCE

Include `code_challenge` and `code_challenge_method` (`plain` or `S256`) in the authorization request, and `code_verifier` in the token exchange.

### Client credentials

Request tokens for service-to-service flows using `grant_type=client_credentials` with `scope=https://graph.microsoft.com/.default`. Returns an `access_token` only (no `refresh_token` or `id_token`). The token carries the client's `application_permissions` as Graph `roles`, and any scope other than `/.default` is `invalid_scope` (`AADSTS1002012`).

A scope for another resource, such as `https://contoso.sharepoint.com/.default`, gives a token for that resource whose `roles` come from `resource_permissions` for that resource URI. Without a grant there, the `roles` claim is left out, as Entra does.

Certificate credentials (`private_key_jwt`) work too: register the certificate's public PEM in `certificates` and send `client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer` with a `client_assertion` signed by its key, with an `x5t#S256` (or `x5t`) header naming the certificate and `iss` and `sub` set to the client id. MSAL's `clientCertificate` option does this. The token gets `appidacr: "2"`. Unknown certificates, bad signatures, or a mismatched issuer answer `401 invalid_client` with the matching AADSTS code; five minutes of clock skew are tolerated.

```json theme={null}
{
  "microsoft": {
    "oauth_clients": [
      {
        "client_id": "example-client-id",
        "name": "SharePoint Sync",
        "certificates": ["-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n"],
        "resource_permissions": {
          "https://contoso.sharepoint.com": ["Sites.FullControl.All"]
        }
      }
    ],
    "graph": {
      "strict_tenants": true
    }
  }
}
```

* `strict_tenants`: unknown directory ids get AADSTS90002 from the token endpoint

### Refresh tokens

```bash theme={null}
curl -X POST http://localhost:4005/oauth2/v2.0/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "refresh_token=r_microsoft_...&\
client_id=example-client-id&\
client_secret=example-client-secret&\
grant_type=refresh_token"
```

Refresh requests require the `client_id` and `client_secret` of the client that received the refresh token. Successful requests rotate the refresh token. Legacy refresh records without a stored client binding remain supported.

### ID tokens

The `id_token` is an RS256 JWT containing `sub`, `oid`, `tid` (tenant ID), `email`, `name`, `preferred_username`, `ver` ("2.0"), and optional `nonce`.

### Signing key

ID tokens are signed with a key stored in emulator state (`kid`: `baseline-microsoft-1`), so they stay valid across snapshot restores and persisted restarts. Set `microsoft.signing_key.private_jwk` to an RSA private JWK to pin the key. See [OIDC signing keys](/configuration.md#oidc-signing-keys).

## Microsoft Graph

Graph v1.0 and beta are served on the same port as sign-in, at `/v1.0/...` and `/beta/...`. Every Graph response carries `request-id`, `client-request-id` (echoed when sent), and `@odata.context`, and errors use Graph's envelope: `{ "error": { "code", "message", "innerError": { "date", "request-id", "client-request-id" } } }`.

### What is emulated

The Graph surface is Outlook in one mailbox store, so a change made through one endpoint shows up in every related one (an invite is a calendar item and an Inbox message; declining removes the event and updates the organizer). Behavior was recorded from a real Microsoft 365 tenant and the recordings are replayed against the emulator on every test run (see [Fidelity](#fidelity)).

* **Directory**: `/me` and `/users` with `$select`, `$filter`, `$orderby`, `$search`, `$count`, `$top` and `$skiptoken` under Graph's directory rules (default and advanced queries with `ConsistencyLevel: eventual`), `User.ReadBasic.All` field limits, and user writes: `POST /users`, `PATCH` (self-service fields on `/me`), `DELETE`.
* **Mailboxes** for every seeded user and shared mailbox, with the well-known folders (Inbox, Drafts, Sent Items, Deleted Items, Junk Email, Archive, Outbox and others), plus mailbox settings and master categories.
* **Folders**: list, get, create, update, delete, child folders, move, copy, permanentDelete, delta and `$count`.
* **Messages**:
  * list, get, `$value` (MIME), create (JSON or base64 MIME), update, delete, permanentDelete, move and copy;
  * delta queries per folder;
  * flags, categories, importance, inference classification and internet message headers;
  * change keys and etags that advance on every change.
* **Sending**:
  * `send`, `sendMail`, reply, replyAll and forward, plus their `create*` draft variants;
  * delivery into recipients' Inboxes with threading (`conversationId`, `conversationIndex`, `In-Reply-To`);
  * Sent Items copies, NDRs for unknown internal recipients, and read and not-read receipts;
  * Inbox rules applied at delivery, automatic replies from `mailboxSettings`, focused or other classification from per-sender overrides, and asynchronous post-delivery processing.
* **Attachments**: file and item attachments, inline images, `$value`, and large file upload sessions with chunked PUTs to the upload URL.
* **Search folders** (`mailSearchFolder`): dynamic membership from `filterQuery` over source folders, counts, PATCH of the definition, and the errors for writes into a search folder.
* **Calendar**:
  * events CRUD on the default and secondary calendars, calendar groups, `calendarView`, `instances` and `calendarView/delta`;
  * Windows and IANA time zones with DST, `Prefer: outlook.timezone`, all-day events;
  * recurring series (all pattern and range types) expanded in the recurrence time zone, occurrence ids, exceptions and cancelled occurrences;
  * meetings: invites delivered as `eventMessageRequest` with a tentative copy in each internal attendee's calendar, accept, tentativelyAccept (with proposals), decline, cancel, forward, organizer updates and response tracking;
  * `getSchedule`, `findMeetingTimes` (with room availability from `locationConstraint`), `reminderView`, dismiss and snooze, file and item attachments with upload sessions, and open extensions;
  * `$expand=microsoft.graph.eventMessage/event` from a meeting message to its calendar item.
* **Calendar sharing**: `calendarPermissions` on any calendar. Sharees with `Calendars.Read.Shared` or `Calendars.ReadWrite.Shared` reach exactly the shared calendars through `/users/{owner}/...`, and each share or role change sends the sharee a sharing message with a copy in the owner's Sent Items.
* **Rooms and places**: `/places/microsoft.graph.room`, `/places/microsoft.graph.roomList`, rooms of a list, get and PATCH. Rooms are resource mailboxes: they appear in free/busy, accept a meeting request when free and decline it on a conflict, and `findMeetingTimes` with `suggestLocation` offers the free ones that belong to a room list. Requests more than 180 days out are declined.
* **Microsoft 365 groups**: `/groups`, members and owners, `memberOf` and `transitiveMemberOf`, and the group calendar (`/groups/{id}/events`, `/calendar`, `/calendarView`) with the group as organizer.
* **Microsoft To Do**: task lists, tasks, checklist items, linked resources, file attachments, delta and change notifications, with the optional Flagged Emails list synced from message flags. The retired beta `/outlook/tasks`, `taskFolders` and `taskGroups` read and write the same tasks.
* **Contacts**: contacts and contact folders (with child folders) CRUD, computed `displayName`, `$filter` including `emailAddresses/any(...)`, delta, photos, open extensions and permanentDelete.
* **Lists and ids**: every Windows time zone from `supportedTimeZones` (or IANA with `TimeZoneStandard=Iana`), `supportedLanguages`, and `translateExchangeIds` for all five id types.
* **Inbox rules** (`messageRules`) and **inference classification overrides**.
* **OData**:
  * `$select`, `$filter` (comparison, logical, `startswith`, `contains`, lambda `any` and `all` on collections such as recipients and categories), `$orderby` with the same "inefficient filter" restrictions as Exchange;
  * `$top`, `$skip`, `$count`, `$search`, `$expand=attachments`;
  * server-driven paging through `@odata.nextLink`.
* **Protocol**:
  * `/v1.0` and `/beta`, case-insensitive segments, and `users('...')` key syntax;
  * `$batch` with `dependsOn`;
  * Graph error bodies and the `request-id`, `client-request-id` and `x-ms-ags-diagnostic` headers;
  * `Prefer: IdType="ImmutableId"` and `outlook.body-content-type`.
* **Change notifications**: `/subscriptions` CRUD with the notification URL validation handshake (for `lifecycleNotificationUrl` too) and the 10070 minute expiration limit. Notifications for created, updated and deleted messages, folders, events, contacts and To Do tasks are POSTed to your endpoint in the shapes recorded from the real service.
* **Identity and permissions**:
  * access tokens are RS256 JWTs with `scp` (delegated) or `roles` (app-only); `scp` holds every scope the user has consented to for the client, as in Entra ID;
  * every operation checks the least-privileged Graph permission;
  * app-only tokens get `/me` errors, and application access policies can restrict them to some mailboxes;
  * delegated access to other mailboxes follows `FullAccess`, `SendAs` and `SendOnBehalf` grants.

### Directory queries

`PATCH /me` takes the self-service fields such as `jobTitle`, `mobilePhone`, `businessPhones`, `officeLocation`, and `preferredLanguage`.

Seeded users are the same records the sign-in picker shows, so a user's Graph `id` is the `oid` in their ID token. `/users` takes `$select`, `$filter`, `$top` (1 to 999), `$orderby`, `$count`, `$search`, and pages with `@odata.nextLink` (`$skiptoken`). Filters follow Graph's directory rules per property: a default query allows `eq`, `in`, and `startswith` on names, mail, and identifiers, and `any()` on `businessPhones`, `proxyAddresses`, and `otherMails`; `ne`, `not`, `endsWith`, range operators, sorting with a filter, and properties such as `department` or `city` need an advanced query (`ConsistencyLevel: eventual` plus `$count=true` or `$search`). Errors otherwise are Graph's (`Request_UnsupportedQuery`, `Directory_ExpiredPageToken`). An app with only `User.ReadBasic.All` sees the basic profile fields. `POST /users` needs `accountEnabled`, `displayName`, `mailNickname`, `userPrincipalName` on a verified domain (a seeded user's domain or one in `verified_domains`), and a `passwordProfile` whose password meets Entra's complexity rule.

### Mail lists and ids

Each user and shared mailbox has Graph's distinguished folders (`inbox`, `drafts`, `sentitems`, `deleteditems`, `archive`, `junkemail`, `outbox`, and the rest) under `msgfolderroot`, and every mailbox route works under `/me` and under `/users/{id | userPrincipalName}`. Lists take Exchange's OData rules, page with `@odata.nextLink`, and default to 10 items. As on Exchange, moving a message gives it a new `id`, every change gives it a new `changeKey`, and `DELETE` removes a message rather than moving it to Deleted Items.

### Paths

Microsoft Graph (`/v1.0` and `/beta`; `{user}` is `me`, `users/{id}` or `users/{userPrincipalName}`):

* `GET`, `PATCH /me`; `GET`, `POST /users`; `GET /users/$count`; `GET`, `PATCH`, `DELETE /users/{id}`
* `/{user}/memberOf[/microsoft.graph.group]`, `/{user}/transitiveMemberOf[/microsoft.graph.group]`
* `/{user}/mailFolders[/{id}][/childFolders|/messages|/move|/copy|/permanentDelete]`, `/{user}/mailFolders/delta`
* `/{user}/messages[/{id}]`, `/{user}/mailFolders/{id}/messages/delta`, `/{user}/messages/{id}/$value`
* `/{user}/messages/{id}/{send|reply|replyAll|forward|createReply|createReplyAll|createForward|move|copy|permanentDelete}`
* `/{user}/sendMail`
* `/{user}/messages/{id}/attachments[/{id}][/$value]`, `/{user}/messages/{id}/attachments/createUploadSession`
* `/{user}/messages/{id}/extensions[/{id}]`
* `/{user}/mailFolders/inbox/messageRules[/{id}]`, `/{user}/inferenceClassification/overrides[/{id}]`
* `/{user}/mailboxSettings`, `/{user}/outlook/masterCategories`, `/{user}/outlook/supportedTimeZones`, `/{user}/outlook/supportedLanguages`
* `/{user}/translateExchangeIds`
* `/{user}/events[/{id}]`, `/{user}/events/{id}/{accept|tentativelyAccept|decline|cancel|forward|dismissReminder|snoozeReminder|permanentDelete|instances|attachments|extensions}`
* `/{user}/calendar[/events|/calendarView|/getSchedule]`, `/{user}/calendars[/{id}[/events|/calendarView]]`, `/{user}/calendarGroups[/{id}[/calendars]]`
* `/{user}/calendarView[/delta]`, `/{user}/findMeetingTimes`, `/{user}/reminderView(startDateTime='...',endDateTime='...')`
* `/{user}/contacts[/{id}[/photo|/extensions|/permanentDelete]]`, `/{user}/contacts/delta`
* `/{user}/contactFolders[/{id}[/childFolders|/contacts]]`, `/{user}/contactFolders/delta`
* `/{user}/calendar[s/{id}]/calendarPermissions[/{id}]`
* `/places/microsoft.graph.room`, `/places/microsoft.graph.roomList`, `/places/{list}/microsoft.graph.roomList/rooms`, `/places/{id|email}`
* `/groups[/{id}[/members|/owners]]` (GET, POST, PATCH, DELETE), `/directory/deletedItems/{id}`, `/groups/{id}/events[/{id}]`, `/groups/{id}/calendar[/events|/calendarView]`, `/groups/{id}/calendarView`
* `/{user}/todo/lists[/{id}[/tasks[/{id}[/checklistItems|/linkedResources|/attachments]]]]`, `/{user}/todo/lists/delta`, `.../tasks/delta`
* `/beta/{user}/outlook/tasks[/{id}[/complete]]`, `/beta/{user}/outlook/taskFolders[/{id}/tasks]`, `/beta/{user}/outlook/taskGroups`
* `/subscriptions[/{id}]`
* `POST /$batch`

The generated coverage matrix in [`docs/coverage.md`](https://github.com/deploybaseline/cli/blob/main/packages/@deploybaseline/microsoft/docs/coverage.md) lists every operation with its permissions and evidence.

### Delivering mail in tests

Mail is delivered `graph.delivery_delay_ms` after a send (default 1000), as Exchange delivers asynchronously. Seed `graph: { delivery_delay_ms: 0 }` to deliver before the send call returns, or call `flushTransport(store)` from `@deploybaseline/microsoft` to deliver everything pending.

Programmatic use can deliver pending mail without waiting for the delay:

```ts theme={null}
import { flushTransport } from "@deploybaseline/microsoft";

flushTransport(store, 1000); // delivers all pending mail now, plus background work due within 1s
```

### Fidelity

Behavior is specified as rules in `conformance/spec/catalog.ts`. Each rule is marked *verified* (a committed recording from a real Microsoft 365 test tenant backs it), *observed* (real service traffic recorded by others, see `docs/research/`), *documented* (from Microsoft documentation only) or *assumed*. The conformance suite replays every recorded scenario against the emulator and compares normalized responses step by step; it runs with the package tests. See [`conformance/README.md`](https://github.com/deploybaseline/cli/blob/main/packages/@deploybaseline/microsoft/conformance/README.md) for the methodology, how to record against your own tenant, and [`docs/coverage.md`](https://github.com/deploybaseline/cli/blob/main/packages/@deploybaseline/microsoft/docs/coverage.md) for current results.

## SharePoint and OneDrive

A stateful SharePoint Online served through Microsoft Graph (`/v1.0`, `/beta`) and the SharePoint REST API (`/{site}/_api`) from one state. A file written through Graph is readable through REST at once with the same ids, versions and permissions, and the reverse. SharePoint routes answer only after a site exists, from the seed config or the control plane; until then they answer like any path Graph does not know.

### Seed config for SharePoint

```json theme={null}
{
  "microsoft": {
    "users": [
      {
        "email": "alice@contoso.com",
        "name": "Alice"
      },
      {
        "email": "bob@contoso.com",
        "name": "Bob"
      }
    ],
    "oauth_clients": [
      {
        "client_id": "sp-app",
        "client_secret": "sp-secret",
        "name": "SharePoint App",
        "redirect_uris": ["http://localhost:3000/callback"],
        "application_permissions": ["Sites.FullControl.All"],
        "delegated_permissions": [
          "Sites.ReadWrite.All",
          "Files.ReadWrite.All",
          "AllSites.FullControl"
        ],
        "certificates": [],
        "resource_permissions": {
          "https://contoso.sharepoint.com": ["Sites.FullControl.All"]
        }
      }
    ],
    "sharepoint": {
      "hostname": "contoso.sharepoint.com",
      "sites": [
        {
          "url": "/sites/projects",
          "title": "Projects",
          "owners": ["alice@contoso.com"],
          "members": ["bob@contoso.com"],
          "lists": [
            {
              "title": "Documents",
              "template": "documentLibrary",
              "url": "Shared Documents",
              "versioning": {
                "major": true,
                "minor": true
              },
              "folders": ["Reports/2026"],
              "files": [
                {
                  "path": "Reports/2026/plan.txt",
                  "content": "Q3 plan"
                }
              ]
            },
            {
              "title": "Tasks",
              "columns": [
                {
                  "name": "Status",
                  "type": "choice",
                  "choices": ["Open", "Done"],
                  "indexed": true
                }
              ],
              "items": [
                {
                  "Title": "Write the brief",
                  "Status": "Open"
                }
              ]
            }
          ],
          "subsites": [
            {
              "url": "/sites/projects/archive",
              "title": "Archive",
              "unique_permissions": true
            }
          ],
          "grants": [
            {
              "app_id": "sp-app",
              "roles": ["read"]
            }
          ]
        }
      ]
    }
  }
}
```

* `application_permissions`: Graph app roles
* `certificates`: app-only REST needs a certificate credential, as in Entra
* `https://contoso.sharepoint.com` under `resource_permissions`: SharePoint app roles
* `owners`: Full Control; the first owner is the site collection admin
* `members`: Edit
* `grants`: Sites.Selected grants for an app

Sites take `group` (a group from `groups:` makes a group-connected team site), `visitors`, `subsites`, `custom_script` and `grants` (`{app_id, roles, list?, item?}`). Lists take `template` (`genericList`, `documentLibrary` or a template number), `url`, `description`, `folders`, `files` (`path`, `content` or `content_base64`, `fields`, `author`), `items`, `versioning`, `permissions` (break inheritance: `{email: read|contribute|edit|design|full}`) and `columns` of type `text`, `note`, `number`, `currency`, `choice`, `multichoice`, `dateTime`, `dateOnly`, `boolean`, `person`, `personMulti`, `lookup` or `hyperlink` (or a Graph `columnDefinition` under `graph`). The other `sharepoint` options are in the [seed config table](#seed-config).

### Tokens

* Graph: scope `https://graph.microsoft.com/.default` (app) or Graph scopes (delegated), with `Sites.*` and `Files.*` permissions.
* REST: scope `https://contoso.sharepoint.com/.default`. The token audience is the SharePoint app id `00000003-0000-0ff1-ce00-000000000000`, as Entra issues it, and its roles or scopes are SharePoint's (`Sites.FullControl.All`, `AllSites.Manage` and the rest). App-only REST refuses client secret tokens (`401` "Unsupported app only token.") and accepts certificate assertions (see [Client credentials](#client-credentials)).

### Graph

```bash theme={null}
TOKEN=... # client credentials token for https://graph.microsoft.com/.default
curl http://localhost:4005/v1.0/sites/contoso.sharepoint.com:/sites/projects -H "Authorization: Bearer $TOKEN"
curl -X PUT "http://localhost:4005/v1.0/sites/{site-id}/drive/root:/notes.txt:/content" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: text/plain" --data "hello"
curl "http://localhost:4005/v1.0/sites/{site-id}/lists/{list-id}/items?\$expand=fields" -H "Authorization: Bearer $TOKEN"
```

* **Sites**: `/sites` (search), `/sites/root`, `/sites/{host}:/{path}`, composite ids, `/sites/getAllSites()`, subsites, site columns, content types, and `/sites/{id}/permissions` (Sites.Selected grants).
* **Lists**: lists, columns, content types (`addCopy`), list permissions, and list items with typed `fields`, `delta`, versions (`restoreVersion`), permissions and `createLink`.
* **Drives**: `/drives/{id}`, a site's `/drive` and `/drives`, a list's `/drive`, `/groups/{id}/drive`, and drive search.
* **Drive items**: by id or path (`root:/a/b.txt:`), children, `content` (simple upload and download), `createUploadSession`, `copy` with a monitor URL, move and rename through `PATCH`, versions, `checkout`, `checkin`, `discardCheckout`, `publish`, `delta`, `search`, `createLink`, `invite` and permissions.
* **OneDrive**: `/me/drive`, `/me/drives`, `/users/{id}/drive` and `/users/{id}/drives` address a `business` drive named `OneDrive`, the Documents library of `https://contoso-my.sharepoint.com/personal/{upn with _}`, created on first use. Other delegated users get `403 accessDenied`.
* **Shares**: `/shares/{id}` for `u!` encoded sharing links and item URLs, with `driveItem`, `root`, `listItem`, `list`, `site` and `permission`.
* **Search and notifications**: `/search/query` for sites, lists, list items, drives and drive items, and `/subscriptions` on drive roots and lists (`updated` only, as Graph documents), notified from the SharePoint change log. `$batch` carries SharePoint answers with their `SPLogId` and `Cache-Control` headers.

### REST

```bash theme={null}
SPTOKEN=... # token for https://contoso.sharepoint.com/.default
curl "http://localhost:4005/sites/projects/_api/web/lists/getbytitle('Tasks')/items" \
  -H "Authorization: Bearer $SPTOKEN" -H "Accept: application/json;odata=nometadata"
curl "http://localhost:4005/sites/projects/_api/web/GetFileByServerRelativePath(decodedurl='/sites/projects/Shared Documents/notes.txt')/\$value" \
  -H "Authorization: Bearer $SPTOKEN"
```

Supported: `contextinfo`, `web`, `site`, lists, items, fields (`addfield`, `CreateFieldAsXml`), content types, folders, files (upload, `$value`, chunked `StartUpload`, `ContinueUpload`, `FinishUpload` and `CancelUpload`, `SP.MoveCopyUtil`), versions, check out and in, publish and content approval, attachments, role assignments and inheritance, effective permissions, site groups and users, recycle bin (both stages), `GetChanges`, `GetListItemChangesSinceToken`, `RenderListDataAsStream`, CAML `GetItems`, search, list webhooks and multipart `$batch`. Answers are JSON (`odata=verbose`, `minimalmetadata` or `nometadata`) or Atom when the `Accept` header names no JSON flavor. PnPjs works with `spfi("<emulator origin>/sites/projects")`, and the package tests run PnPjs and the Kiota Graph SDK (`drives` and `sites`) against the emulator.

Graph hands out download, upload session and copy monitor URLs on the SharePoint host, signed with `tempauth` and taking no Authorization header:

* `GET /sites/:site/_layouts/15/download.aspx` — Download a file (also `HEAD`)
* `PUT /_api/v2.0/drives/:driveId/items/:itemId/uploadSession` — Upload a byte range to a session (`GET` reads its status, an empty `POST` commits a deferred session, `DELETE` cancels it)
* `GET /_api/v2.0/monitor/:monitorId` — Read a folder copy's status
* `GET /sites/:site/_api/v2.0/drives/:driveId/items/:itemId` — Read a file copy's status (`force303=1`, the new item's v2.0 URL)

### Control plane

* `GET /_baseline/sharepoint` — Summary of sites, lists, jobs and sessions
* `POST /_baseline/sharepoint/sites` — Provision a site, in the format of a `sites` entry
* `POST /_baseline/sharepoint/clock` — `{advance_ms}`, `{freeze: true}` or `{real: true}`; drives search lag, upload session expiry and copy jobs
* `POST /_baseline/sharepoint/jobs/run` — Finish pending copy jobs
* `POST /_baseline/sharepoint/changes/trim` — `{keep_last: n}`; older delta and change tokens then need a resync
* `GET /_baseline/sharepoint/faults` — List fault rules
* `POST /_baseline/sharepoint/faults` — Add a fault rule: `{api: graph|rest, method, path (regex), status, count, retry_after, code, message}`
* `DELETE /_baseline/sharepoint/faults` — Remove every fault rule, or one with `?id=`
* `GET /_baseline/sharepoint/snapshot` — SharePoint state as JSON
* `POST /_baseline/sharepoint/restore` — Replace SharePoint state with a snapshot

### Behavior worth knowing

| Situation | Response |
| - | - |
| REST call without a token | `403` `System.UnauthorizedAccessException` (verified live) |
| REST item `DELETE` without `If-Match` | `412` (verified live); file `DELETE` needs no `If-Match` |
| REST list `DELETE` | Needs `If-Match` and deletes for good. Graph list `DELETE` and REST `recycle()` move it to the recycle bin (verified live) |
| App holding only Sites.Selected without a grant | `403 accessDenied` (verified live) |
| Reading a file someone else has checked out | The checked-in version: content, fields, `UIVersionLabel` and an empty history. Only the holder sees the working copy (verified live) |
| Reader who cannot see drafts (`DraftVersionVisibility` 1 or 2) | The last published version's content, label and `Modified` (verified live) |
| `sites/{id}/getByPath(path=...)` | `400 BadRequest`; use `/sites/{host}:/{path}` (verified live) |
| `POST driveItem/restore` on a library | `400 notSupported` (verified live) |
| Upload session past `expirationDateTime` (15 minutes, extended by each fragment) | Accepted for `upload_session_grace_ms` more (one hour by default), as the service was seen to; set `0` to refuse at the stated time |
| `DELETE` on a REST site group; `siteusers({id})` | `400`; remove with `sitegroups/removebyid`. `siteusers` is keyed by login name, so use `getuserbyid` (verified live) |
| `breakroleinheritance` on a site collection's root web | Nothing changes: the root web already has unique permissions |
| Search right after a write | Not found until `search_index_delay_ms` of virtual time passes |
| An operation the emulator does not model | `501`, never a fabricated success |

Graph permission ids are the base64 of the principal's login name; SharePoint groups carry a `sharePointGroup` facet and site collection administrators list as owners. `invite` adds a role assignment the invited user can use at once. File and folder names follow the service's rules on each surface: Graph and REST refuse the same names with their own messages, REST methods that take a URL (`Files/add(url=)`, `folders/add`) trim trailing dots and spaces while `AddUsingPath` refuses them, and a REST request path longer than 401 characters or holding `*`, `<`, `>` or `?` answers `401` as the service's web server does. Recycling, deleting or moving a list's root folder acts on the list itself. With `allow_subsite_creation: false`, `web/webs/add` answers `400 InvalidOperationException`.

Thirty-five SharePoint scenarios are recorded against a real tenant and replayed against the emulator. The operation inventory, generated coverage report, behavioral catalog with evidence, decisions and gap analysis are in [`docs/sharepoint`](https://github.com/deploybaseline/cli/tree/main/packages/@deploybaseline/microsoft/docs/sharepoint).

## Seed config

A sign-in only config:

```json theme={null}
{
  "microsoft": {
    "users": [
      {
        "email": "testuser@outlook.com",
        "name": "Test User",
        "given_name": "Test",
        "family_name": "User",
        "tenant_id": "9188040d-6c67-4c5b-b112-36a304b66dad"
      }
    ],
    "oauth_clients": [
      {
        "client_id": "example-client-id",
        "client_secret": "example-client-secret",
        "name": "My Microsoft App",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/microsoft-entra-id"],
        "tenant_id": "9188040d-6c67-4c5b-b112-36a304b66dad"
      }
    ]
  }
}
```

The Microsoft Graph side (Outlook mail, calendar, contacts, To Do, places, groups, and the directory) takes more keys: Graph profile fields on users, `shared_mailboxes`, `mailbox_permissions`, `verified_domains`, `messages`, `rooms`, `room_lists`, `groups`, and `graph` options. A fuller example:

```json theme={null}
{
  "microsoft": {
    "tenant_id": "11111111-2222-3333-4444-555555555555",
    "users": [
      {
        "email": "alice@contoso.test",
        "name": "Alice Example",
        "given_name": "Alice",
        "family_name": "Example",
        "job_title": "Engineer",
        "department": "Research"
      },
      {
        "email": "bob@contoso.test",
        "name": "Bob Example"
      }
    ],
    "shared_mailboxes": [
      {
        "email": "support@contoso.test",
        "name": "Support"
      }
    ],
    "mailbox_permissions": [
      {
        "mailbox": "support@contoso.test",
        "user": "alice@contoso.test",
        "rights": ["FullAccess", "SendAs"]
      }
    ],
    "oauth_clients": [
      {
        "client_id": "example-client-id",
        "client_secret": "example-client-secret",
        "name": "My Microsoft App",
        "redirect_uris": ["http://localhost:3000/api/auth/callback/microsoft-entra-id"],
        "application_permissions": ["Mail.ReadWrite", "Mail.Send"],
        "delegated_permissions": ["User.Read", "Mail.ReadWrite", "Mail.Send"]
      }
    ],
    "verified_domains": ["fabrikam.test"],
    "messages": [
      {
        "user": "alice@contoso.test",
        "folder": "inbox",
        "from": "bob@contoso.test",
        "to": ["alice@contoso.test"],
        "subject": "Quarterly plan",
        "body": "<p>Draft attached.</p>",
        "body_type": "html"
      }
    ],
    "rooms": [
      {
        "email": "boardroom@contoso.test",
        "name": "Boardroom",
        "capacity": 12,
        "building": "HQ",
        "floor_number": 3,
        "room_lists": ["hq-rooms@contoso.test"]
      }
    ],
    "room_lists": [
      {
        "email": "hq-rooms@contoso.test",
        "name": "HQ rooms"
      }
    ],
    "groups": [
      {
        "email": "design@contoso.test",
        "name": "Design",
        "members": ["alice@contoso.test", "bob@contoso.test"],
        "owners": ["alice@contoso.test"],
        "visibility": "Public"
      }
    ],
    "graph": {
      "delivery_delay_ms": 1000,
      "strict_auth": false,
      "application_access_policies": {
        "example-client-id": ["alice@contoso.test"]
      }
    }
  }
}
```

* `application_permissions`: Optional. Roles in client\_credentials tokens; omit for every modeled permission.
* `delegated_permissions`: Optional. Scopes granted for ".default" delegated requests; omit for every modeled permission.
* `visibility`: Private groups are closed to non-members
* `delivery_delay_ms`: send to delivery delay; 0 delivers synchronously
* `strict_auth`: true rejects unknown bearer tokens with 401
* `application_access_policies`: restrict app-only clients to some mailboxes

| Key | Default | Meaning |
| - | - | - |
| `tenant_id` | `9188040d-6c67-4c5b-b112-36a304b66dad` | Tenant id in tokens, discovery and message headers. |
| `domain` | domain of the first user | Primary SMTP domain. Mail to other addresses in this domain bounces with an NDR. |
| `signing_key` | generated | Pin the token signing key (an RSA private JWK). |
| `users[]` | none | `email`, `name`, `given_name`, `family_name`, `tenant_id` (defaults to the top-level `tenant_id`), `mail` (`null` gives the user no mailbox), `account_enabled`, `job_title`, `department`, `office_location`, `mobile_phone`, `business_phones`, `company_name`, `employee_id`, `city`, `country`, `usage_location`, `preferred_language`, `user_type`. |
| `users[].profile` | none | Other directory properties by their Graph names (for example `employeeType`). They win over the fields above. |
| `users[].id_format` | `compact` | Exchange id form for the mailbox: `compact` (`AQMk...`) or `standard` (`AAMk...`). |
| `users[].outlook_web` | `false` | The user has opened Focused Inbox in Outlook on the web: adds the `OwaFV15.1AllFocused...` and `OwaFV15.1AllOther...` search folders Graph lists with `isSupported: false`. |
| `users[].mailbox_settings` | none | `{ time_zone, locale }` the user chose at first sign-in. Sets `timeZone`, `language` and working hours; without it the mailbox reads as never configured. |
| `shared_mailboxes` | none | Mailboxes without a sign-in account. |
| `mailbox_permissions` | none | Exchange delegation: `FullAccess`, `SendAs`, `SendOnBehalf`. |
| `oauth_clients[]` | none | `client_id`, `client_secret`, `name`, `redirect_uris`, `tenant_id` (defaults to the top-level `tenant_id`), and the permission lists below. |
| `oauth_clients[].application_permissions` | every modeled permission | App roles in client credentials tokens. |
| `oauth_clients[].delegated_permissions` | every modeled permission | Scopes granted for `.default`. |
| `oauth_clients[].certificates` | none | Public PEM certificates for `private_key_jwt` client assertions. |
| `oauth_clients[].resource_permissions` | none | Roles in client credentials tokens for other resources, by resource URI (for example `https://contoso.sharepoint.com`). |
| `verified_domains` | none | Domains `POST /users` accepts in a `userPrincipalName`, besides those of seeded users. |
| `messages` | none | Messages placed in seeded mailboxes: `user`, `folder` (a well-known name or a folder to create), `from`, `to`, `cc`, `subject`, `body`, `body_type`, `is_read`, `importance`, `categories`, `flagged`, `received_at`. |
| `rooms` | none | Room mailboxes for the places API, free/busy and automatic booking. Keys: `email`, `name`, `capacity`, `building`, `floor_number`, `floor_label`, `label`, `phone`, `nickname`, `is_wheelchair_accessible`, `booking_type` (`standard` or `reserved`), `tags`, `audio_device_name`, `video_device_name`, `display_device_name`, `address`, `room_lists`. |
| `room_lists` | none | Room lists: `email`, `name`, `phone`. |
| `groups` | none | Microsoft 365 groups with a group calendar: `email`, `name`, `description`, `members`, `owners`, `visibility`, `team` (true adds the Teams provisioning properties). |
| `graph.delivery_delay_ms` | `1000` | Delay between send and delivery. `0` delivers before the send call returns. |
| `graph.strict_auth` | `false` | Reject unknown bearer tokens. When `false`, an opaque token the emulator did not issue acts as the first seeded user with full access, so quick scripts work without an OAuth flow. JWTs are always verified, and a missing token is always `401`. |
| `graph.application_access_policies` | none | Per client id, the mailboxes app-only tokens may access. |
| `graph.skip_subscription_validation` | `false` | Skip the notification URL validation request. |
| `graph.allow_http_notifications` | `false` | Accept `http://` notification URLs for local receivers (Graph requires HTTPS). |
| `graph.todo_flagged_emails` | `false` | Add the To Do "Flagged Emails" list, synced from message flags. `true` adds it for every mailbox, a list of addresses for those only. |
| `graph.online_meeting_provider` | `unknown` | `teamsForBusiness` lets events become Teams meetings; `unknown` matches a tenant without Teams. |
| `graph.strict_tenants` | `false` | The token endpoint refuses a directory id it does not know (not the home tenant, a client's or a user's) with `400 invalid_tenant` (`AADSTS90002`). |
| `sharepoint.hostname` | `contoso.sharepoint.com` | Tenant SharePoint host. OneDrive uses the `-my` host beside it. |
| `sharepoint.sites` | none | Sites to create (see [SharePoint and OneDrive](#sharepoint-and-onedrive)). SharePoint answers once a site exists. |
| `sharepoint.access_origin` | the emulator base URL | Origin of download, upload session and copy monitor URLs. |
| `sharepoint.copy_delay_ms` | `0` | Virtual time an asynchronous copy takes. |
| `sharepoint.search_index_delay_ms` | `0` | Virtual time before a write is searchable. |
| `sharepoint.upload_session_grace_ms` | `3600000` | How long an upload session stays usable past its `expirationDateTime`, as observed live. `0` refuses at the stated time. |
| `sharepoint.graph_page_size` | `200` | Graph page size for list items. |
| `sharepoint.rest_page_size` | `100` | REST page size for list items. |
| `sharepoint.allow_subsite_creation` | `true` | `false` makes `web/webs/add` answer `400`, as tenants with subsites turned off do. |

## Inspector

* `GET /` — Inspector: the directory, mailboxes, subscriptions, and OAuth apps

Open the emulator's root URL in a browser for the directory (users, shared and room mailboxes, groups), each mailbox's mail, calendar, contacts, and To Do, change notification subscriptions, and OAuth apps.

## Limits

* Timing of asynchronous Exchange processing is approximate.
* Directory query rules for `/users` (which properties and operators need an advanced query) follow Microsoft's documentation and are not recorded live.
* Throttling, delta token expiry and application access policies are documented only and marked so in the coverage matrix.
* Not modeled: private event masking for sharees, delegate meeting forwarding, group conversations, room list based location search beyond the seeded rooms, and To Do list sharing.
* `$search` matches KQL-style terms and properties directly. There is no search index, so no ranking and no indexing delay.
* SharePoint: sites are created by seed config or the control plane, never through Graph (`POST /sites` is not a Graph v1.0 operation). Not modeled: the client object model (`_vti_bin/client.svc`), the SOAP services, the term store, hub sites, site pages and navigation, tenant administration and `SP.AppContextSite`. Those operations answer `501`.

## Conformance

| | |
| - | - |
| Routes | 546 supported or partial, 0 planned |
| Route gate | fail |
| Spec gate | fail, 336 allowed ([microsoftgraph/msgraph-metadata](https://github.com/microsoftgraph/msgraph-metadata) at `1fd0ef5`) |
| SDKs tested | `@azure/identity@4.14.0`, `@azure/msal-node@7.1.0`, `@microsoft/kiota-abstractions@1.0.0-preview.110`, `@microsoft/kiota-authentication-azure@1.0.0-preview.110`, `@microsoft/kiota-http-fetchlibrary@1.0.0-preview.110`, `@microsoft/microsoft-graph-client@3.0.7`, `@microsoft/msgraph-sdk-core@1.0.0-preview.17`, `@microsoft/msgraph-sdk-drives@1.0.0-preview.91`, `@microsoft/msgraph-sdk-sites@1.0.0-preview.91`, `@microsoft/msgraph-sdk-users@1.0.0-preview.91`, `@microsoft/msgraph-sdk@1.0.0-preview.91`, `@pnp/core@4.22.0`, `@pnp/queryable@4.22.0`, `@pnp/sp@4.22.0`, `jose@6.2.12`, `openid-client@6.8.8` |
| Dual mode | verified 2026-10-20 |

| Module | Routes | Partial | Planned |
| - | -: | -: | -: |
| auth | 14 | 0 | 0 |
| graph | 1 | 0 | 0 |
| users | 6 | 0 | 0 |
| groups | 12 | 0 | 0 |
| mail | 49 | 0 | 0 |
| mailbox | 41 | 0 | 0 |
| calendar | 53 | 0 | 0 |
| contacts | 25 | 0 | 0 |
| todo | 27 | 0 | 0 |
| places | 6 | 0 | 0 |
| subscriptions | 5 | 0 | 0 |
| sharepoint | 306 | 4 | 0 |
| inspector | 1 | 0 | 0 |

See [CONFORMANCE.md](https://github.com/deploybaseline/cli/blob/main/CONFORMANCE.md) for every provider.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.