{"_id":"@birtalanrobert/calendars","name":"@birtalanrobert/calendars","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@birtalanrobert/calendars","version":"1.0.0","description":"Two-way calendar sync: our appointments pushed out, their busy time pulled in","license":"AGPL-3.0-only","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","default":"./dist/nestjs/index.js"},"./google":{"types":"./dist/providers/google.d.ts","default":"./dist/providers/google.js"},"./microsoft":{"types":"./dist/providers/microsoft.d.ts","default":"./dist/providers/microsoft.js"},"./package.json":"./package.json"},"publishConfig":{"access":"public"},"dependencies":{"@microsoft/microsoft-graph-client":"^3.0.7","googleapis":"^178.0.0","@birtalanrobert/database":"^1.1.0","@birtalanrobert/tenancy":"^1.1.0","@birtalanrobert/observability":"^1.2.0","@birtalanrobert/http":"^2.0.0"},"peerDependencies":{"@nestjs/common":"^11.0.0","typeorm":"^1.0.0"},"repository":{"type":"git","url":"git+https://github.com/birtalanrobert/mortar.git","directory":"packages/calendars"},"author":{"name":"Robert Birtalan"},"scripts":{"build":"tsc -p tsconfig.build.json","clean":"rm -rf dist *.tsbuildinfo","typecheck":"tsc -p tsconfig.json --noEmit"},"_id":"@birtalanrobert/calendars@1.0.0","bugs":{"url":"https://github.com/birtalanrobert/mortar/issues"},"homepage":"https://github.com/birtalanrobert/mortar#readme","_integrity":"sha512-JhhT4+f3WiylizlPh/KQ0lIZ3cLN6L7gV96luoTbbkGo9CPDJI5rXGxixav6voi10b+04BGZ9pbIcF9aqCiCFg==","_resolved":"/private/var/folders/zx/7dcyg3mn6kjfyzsymzgpx1jr0000gn/T/4141c40b1204a4327f4f44af54745996/birtalanrobert-calendars-1.0.0.tgz","_from":"file:birtalanrobert-calendars-1.0.0.tgz","_nodeVersion":"24.16.0","_npmVersion":"11.13.0","dist":{"integrity":"sha512-JhhT4+f3WiylizlPh/KQ0lIZ3cLN6L7gV96luoTbbkGo9CPDJI5rXGxixav6voi10b+04BGZ9pbIcF9aqCiCFg==","shasum":"161fbe7f93a7da337b8663b16ea1d22d22ee49b2","tarball":"https://registry.npmjs.org/@birtalanrobert/calendars/-/calendars-1.0.0.tgz","fileCount":53,"unpackedSize":274337,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCl/zTsuPGMtlOC8k9SRMFr6X76yfAdRumPC0zbOFMv2gIgTBaHJhoruzDxdf7midD0yRnDvDa3U7OMQr1N6WVX1O0="}]},"_npmUser":{"name":"birtalanrobert","email":"birtalanrobert@gmail.com"},"directories":{},"maintainers":[{"name":"birtalanrobert","email":"birtalanrobert@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/calendars_1.0.0_1788770173314_0.9562425700629904"},"_hasShrinkwrap":false}},"time":{"created":"2026-09-07T08:36:13.049Z","1.0.0":"2026-09-07T08:36:13.460Z","modified":"2026-09-07T08:36:13.712Z"},"maintainers":[{"name":"birtalanrobert","email":"birtalanrobert@gmail.com"}],"description":"Two-way calendar sync: our appointments pushed out, their busy time pulled in","homepage":"https://github.com/birtalanrobert/mortar#readme","repository":{"type":"git","url":"git+https://github.com/birtalanrobert/mortar.git","directory":"packages/calendars"},"author":{"name":"Robert Birtalan"},"bugs":{"url":"https://github.com/birtalanrobert/mortar/issues"},"license":"AGPL-3.0-only","readme":"# @birtalanrobert/calendars\n\nTwo-way calendar sync: our appointments pushed out, their busy time pulled in.\n\nTwo specifications call for it — project 02's Phase 13 and project 08's Phase 2\n— and it is built for both.\n\n## The rules come first\n\nA one-way iCal feed cannot corrupt the diary it exports. **A two-way sync can**,\nwhich is why `sync.ts` exists before any provider code: _what wins when both\nsides change_ has to be a stated rule rather than whichever write arrived last.\n\n1. **The diary owns appointments.** The external calendar holds a copy. If\n   somebody edits or deletes that copy, the copy is wrong — not the appointment\n   — and the next sync puts it back. A customer was told a time; a stylist\n   dragging an event in Google has not told them anything.\n2. **The external calendar owns everything else.** A dentist appointment in\n   somebody's personal calendar is busy time we respect and never touch.\n3. **Disconnecting changes nothing about the diary.** It removes our copies and\n   forgets their busy time. Every appointment stays exactly where it was.\n\nThe asymmetry is the design. \"Last write wins\" between two systems that both\nbelieve they are authoritative is how an appointment quietly moves an hour and a\ncustomer arrives to an empty chair.\n\n## Counting without a database\n\n```ts\nimport { busyFrom, driftOf, shouldPush } from '@birtalanrobert/calendars';\n\n// Four overlapping meetings are one stretch of unavailable time.\nbusyFrom(theirEvents, ourExternalIds);\n\n// Somebody moved our copy. Put it back.\ndriftOf(whatWeWrote, whatIsThereNow); // { kind: 'edited', theirs }\n```\n\nThe root entry point is framework-free and browser-safe. Entities, the service\nand the migration are behind `/nestjs`; the adapters are behind `/google` and\n`/microsoft` so a product using one does not install the other's SDK.\n\n## Wiring it\n\n```ts\nimport {\n  CalendarsService,\n  CALENDAR_PROVIDERS,\n  CALENDAR_SEALING_KEY,\n} from '@birtalanrobert/calendars/nestjs';\nimport { GoogleCalendar } from '@birtalanrobert/calendars/google';\nimport { sealingKey } from '@birtalanrobert/database';\n\nproviders: [\n  CalendarsService,\n  {\n    provide: CALENDAR_PROVIDERS,\n    useValue: new Map([['google', new GoogleCalendar({ clientId, clientSecret })]]),\n  },\n  { provide: CALENDAR_SEALING_KEY, useValue: sealingKey(process.env.CALENDAR_SEALING_KEY!) },\n];\n```\n\n## What it deliberately does not do\n\n- **Store their calendar.** Their events are read in a window, turned into busy\n  periods, and forgotten. Keeping a copy of somebody's personal calendar would\n  be holding a great deal of data about them for no benefit — a data-protection\n  argument before a storage one.\n- **Write into your diary.** `pull` _returns_ busy periods; the product decides\n  what they mean. A shared package that wrote into a product's own tables would\n  be a shared package with a foreign key into them.\n- **Invite anybody.** The copy carries no attendees and no reminders. It is a\n  diary for the person who owns the calendar, not a meeting request for a\n  customer who already booked.\n\n## Two traps, both paid for\n\n**A refresh token arrives once.** Both providers issue one on first consent and\nthen stop sending it. A caller that overwrites the stored token with `undefined`\non every refresh disconnects every calendar within the hour. `connect` refuses a\ngrant with no refresh token for the same reason, rather than accepting one that\ndies at lunchtime.\n\n**Graph returns local time plus a zone name.** Not an instant. Parsed without\napplying the zone it is however many hours out the server happens to be, and it\nlooks entirely plausible on screen.\n","readmeFilename":"README.md","_rev":"1-f441333dc2b7d80169f1130263a5fe2c"}