references/graph-calendar.md — Approval-Gated Calendar

From version 0.1.0 of Approval-Gated Calendar, shown publicly by its creator. 6.9 KiB · SHA-256 b3d64c1d0da7c306a2907929c4856693b5d1ad3e83bbdf6d27c541b5f682e23d.

# Microsoft Graph calendar reference

This reference records the request and response assumptions behind Approval-Gated Calendar. The script targets Microsoft Graph v1.0 and a Microsoft 365 tenant's client-credentials application.

## Authentication and permissions

The client posts `client_id`, `client_secret`, `grant_type=client_credentials`, and `scope=https://graph.microsoft.com/.default` to `https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token`. It sends the resulting bearer token to `https://graph.microsoft.com/v1.0` and keeps credentials and tokens in memory.

Use administrator-consented **application** `Calendars.Read` for read commands and `Calendars.ReadWrite` for `create` and `cancel`. Exchange Online application access policies or RBAC for Applications should restrict the app's mailbox reach. Tenant-wide application consent by itself does not identify an authorized mailbox set. A mailbox selected by `--user` is a target, not a permission check.

## Endpoints

Mailbox identifiers and event ids are URL encoded before entering a path. Query values are URL encoded.

| Command | Method and path | Purpose |
| --- | --- | --- |
| `list` | `GET /users/{user}/calendarView` | Events in the selected `startDateTime` to `endDateTime` interval |
| `search` | `GET /users/{user}/calendarView` | Same interval, then subject matching in the client |
| `show` | `GET /users/{user}/events/{id}` | One event and its current fields |
| `availability` | `POST /users/{user}/calendar/getSchedule` | Schedule items for requested attendees and interval |
| `create` | `POST /users/{user}/events` | New event; attendees may be invited |
| `cancel` | `POST /users/{user}/events/{id}/cancel` | Cancellation with a comment |

`calendarView` can return multiple pages. Follow only guarded Graph pagination links, bounded by the CLI's limits. If a page is skipped or fails, label the result `incomplete`, list the skipped portion, and return status 2 unless `--allow-partial` is selected. Search does not rely on undocumented calendar `$search` behavior; it filters subjects on the client side within the selected interval.

`getSchedule` is a POST with `schedules`, `startTime`, `endTime`, and an availability interval. It is a read in the product workflow, though Graph uses POST. It can disclose free and busy details; share only what the user needs. Calendar responses can vary with mailbox and tenant settings.

## Event fields and approval

The create digest binds the target mailbox and exact organizer, attendee list, subject, time zone, start, end, body, location, and online-meeting flag shown in the dry run. A changed field changes the digest. The dry run is offline and does not create a server draft.

The cancel digest binds target mailbox, event id, current changeKey read by the dry run, and exact comment. Live mode requires both `--change-key` and `--approve DIGEST` before a request, then refetches the event to check that its changeKey is still current. A conflict or HTTP 409/412 does not authorize a retry; preview again and seek fresh approval. A successful cancel response is submission evidence, not proof that every invitee received a notice.

## Errors and transport

The client permits only HTTPS to `graph.microsoft.com` and `login.microsoftonline.com`, checking every redirect. Python urllib honors `HTTPS_PROXY`, `https_proxy`, `NO_PROXY`, `no_proxy`, and applicable macOS or Windows system proxy settings. A selected proxy can carry the request; TLS still terminates at the allowed host with certificate validation. Authentication failure, permission denial, not found, conflicts, and throttling are errors, not empty results.

Do not blindly retry a write after a timeout or uncertain server response. Report `outcome unknown` and reconcile with a read before considering a new action. A fresh create request could duplicate an event or invitation.

The fake transport uses `M365_CALENDAR_FAKE_TRANSPORT` plus `M365_CALENDAR_ALLOW_FAKE=zero-width-lab-test-suite-only`. It reads a synthetic fixture, emits a stderr warning, and does not contact a host. An absent, malformed, or exhausted fixture must fail before a real request. Never place real tenant, mailbox, event, or credential data in fixtures.

## Read scope, transport and limits

Search fetches and parses every returned calendarView page for the selected mailbox and interval before filtering subjects locally; the query does not narrow reads. Pagination accepts equivalent percent-encoding and users('id') endpoint spelling; start/end instants must match. Redirects preserve the selected interval and exact path, and fetched pagination query values are sent back to Graph. Identifiers and remote paths reject traversal, encoded separators and backslashes before a request.

Microsoft Entra at login.microsoftonline.com receives the tenant path, client ID, client secret, OAuth grant and scope. Microsoft Graph at graph.microsoft.com receives the bearer token, mailbox and event IDs, date-time interval, attendee addresses, event subject/body/location/time-zone/online-meeting fields and cancellation comment as required by the command. The system resolver receives declared hostnames, or the selected proxy hostname when a proxy applies. No attendee-domain lookup occurs. Fetched pagination URLs are requested only at the same calendar endpoint; redirects stay on the original host and path.

Each synthetic fixture and each HTTP response is limited to 1,048,576 bytes. JSON nesting is limited to 64 levels; non-finite constants are rejected. Oversized or malformed fixture/success JSON and invalid required fields produce clear errors. Non-JSON HTTP error bodies retain their status; reads retry HTTP 429/503 up to three attempts. There is no stdin input. Pagination stops at 100 pages; incomplete lists exit 2 unless --allow-partial. No local file or directory is created. Python 3.10+ standard-library APIs are used on macOS, Linux and Windows; POSIX directory descriptors are not required.

## Verification limits

Cancellation refetches and compares changeKey before POST. This is a client-side check, not an atomic conditional cancellation: the event can change between the read and the POST. No provider If-Match guarantee is claimed. HTTP 409/412 are reported if received. Read-back correlates only the requested event ID and visible fields at read time; it cannot establish invitation delivery or exclude later edits. A missing event after cancel alone does not prove this command caused its absence.

```text
python3 scripts/approval_gated_calendar.py show --user MAILBOX RETURNED_EVENT_ID --json
python3 scripts/approval_gated_calendar.py cancel EVENT_ID --user MAILBOX --comment 'The meeting is cancelled.' --change-key PREVIEW_CHANGE_KEY --approve PREVIEW_DIGEST --json
python3 scripts/approval_gated_calendar.py show --user MAILBOX EVENT_ID --json
```
Replace each uppercase placeholder with the exact approved mailbox, returned ID, preview changeKey and digest. Verification is a separate caller-run read, not an automatic CLI feature.

By Zero Width Lab (zerowidthlab.com)

License: MIT (read it).

MITMIT License

Copyright (c) 2026 Zero Width Lab

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

Back to the listing