feat: calendar subscription backend / ICS feed (Slice 4A of calendar sync payload) #246
No reviewers
Labels
No labels
agent:hermes
bug
chore
dependency
feature
status:blocked
status:in-progress
status:needs-info
status:needs-review
status:ready
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
owlburtoe/Shiftd!246
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/calendar-sync-slice-4a-ics-feed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Slice 4A of the Calendar Sync Payload enrichment (spec: docs/superpowers/specs/2026-05-28-calendar-sync-payload-design.md, Section C). Token-authenticated ICS subscription feed plus the dept-scoped endpoints that mint/rotate the token. Builds on merged Slices 1 (#225 facility address), 2 (#242 tags), 3 (#244 iOS). Backend + docs only; no migration-ordering clash with concurrent work (0055 is the only new migration).
What it adds
staff.calendar_feed_token varchar(64) UNIQUE NULL+ migration0055.repositories.ts):getOrCreateCalendarFeedToken(atomic lazy mint — concurrent first reads converge on one token viaWHERE … IS NULL+ row lock),rotateCalendarFeedToken(immediate invalidation),resolveCalendarFeed(staff+dept+facility join, 404 on miss/inactive/broken).GET /api/d/{deptSlug}/staff/me/calendar-subscription(lazy mint →{ url }) andPOST …/regenerate(rotate, auditedcalendar_feed.token_rotatedwith no token value).GET /api/calendar/:token.ics— mounted before auth middleware, resolves everything from the token, builds today..+180 (dept tz) excluding declined, reuses Slice 2'sgetUpcomingAssignmentsForStaff(tags + frozen timing + status) and Slice 1's facility address forLOCATION. Headers per spec; rate-limited (misses by IP viaskipSuccessfulRequests, hits by token).lib/calendar/ics.ts, pure): RFC 5545 escaping + 75-octet folding,formatCalendarTitle/Notesparity-locked to the shared fixture (same file the iOS formatters use), VEVENT with overnight roll and all-dayVALUE=DATE.buildCalendarFeedUrl(no hand-concat; URL guard passes). OpenAPI + SECURITY.md.VTIMEZONE decision (spec Risk 1, resolved)
Hand-rolled ICS with floating local time (
DTSTART:YYYYMMDDTHHMMSS, noZ/TZID). No new dependency; preserves the facility-authored wall-clock for the common in-facility subscriber and avoids the UTC→wrong-wall-clock failure mode. A hand-rolled VTIMEZONE was avoided as a footgun; absolute-instant semantics for cross-tz travel are out of v1 scope. Documented in SECURITY.md + the spec.Test plan
tsc --noEmit,check:runtime-contracts,check:migration-ordering,check:url-generation,smoke:backend-runtimeall passSecurity
Token is a bearer credential replacing auth; URL/proxy-log exposure accepted v1 (GitHub/GitLab ICS model), never logged by the app, never in audit metadata. 404 (no detail) on all misses. Feed carries only shift/schedule/tags/time/address — no coworker or contact PII.
Follow-ups
4B (web subscription UI) and 4C (iOS subscription UI) depend on this.