feat: add nostr calendar integration#24266
Conversation
Adds Nostr calendar integration supporting NIP-52 calendar events. Features: - Bunker (NIP-46) and nsec authentication - Private calendar events with gift wrap (NIP-59) - Availability blocks (kind 31927) for busy time - Auto-discovery of relay list from kind 10002 - Full CRUD operations for calendar events Implements NIPs: 9, 44, 46, 52, 59
|
@patrickulrich is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
|
|
WalkthroughThis change adds a new “nostrcalendar” app integration. It registers setup routing and SSR mappings, app-store metadata, keys/data schemas, API handlers, and calendar service resolution entries. A new setup UI is introduced with bunker vs nsec authentication flows posting to /api/integrations/nostrcalendar/add. Server-side props redirect unauthenticated users. Backend adds Nostr credential validation/storage, encryption, and redirects after install. A NostrCalendarService implements calendar operations via a new NostrClient supporting NIP-52 events, relay discovery, private gift-wrapped events, availability blocks, and deletions. A BunkerManager handles NIP-46 connections, validation, and permissions. Package config, description, and public exports are included. Possibly related PRs
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Warning Review ran into problems🔥 ProblemsErrors were encountered while retrieving linked issues. Errors (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
packages/app-store/nostrcalendar/DESCRIPTION.md (1)
1-52: Excellent documentation with one suggestion.The documentation is comprehensive and clearly explains the integration's features, privacy model, and setup process.
Consider adding a section about the
CALENDSO_ENCRYPTION_KEYenvironment variable requirement mentioned in the PR description, especially since it's critical for the encryption of nsec keys.Add an "Environment Variables" or "Requirements" section:
+## Requirements + +The following environment variable must be configured: + +- `CALENDSO_ENCRYPTION_KEY` - A base64-encoded 32-byte key used for encrypting credentials. Required for both bunker and nsec authentication methods. + ## Setup 1. Install the app from the Cal.com app storeapps/web/components/apps/nostrcalendar/Setup.tsx (1)
47-158: Localize user-facing copy witht()Coding guidelines require wrapping frontend text in
t()(or equivalent). This component ships several hard-coded English strings (“Connect to Nostr”, field labels, helper text, permissions copy, etc.), so the screen won’t localize. Please run the copy throught()(or add keys to your namespace) before shipping.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Disabled knowledge base sources:
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (5)
packages/app-store/nostrcalendar/static/1.jpegis excluded by!**/*.jpegpackages/app-store/nostrcalendar/static/2.jpegis excluded by!**/*.jpegpackages/app-store/nostrcalendar/static/3.jpegis excluded by!**/*.jpegpackages/app-store/nostrcalendar/static/icon.svgis excluded by!**/*.svgyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (22)
apps/web/components/apps/AppSetupPage.tsx(1 hunks)apps/web/components/apps/nostrcalendar/Setup.tsx(1 hunks)packages/app-store/_pages/setup/_getServerSideProps.tsx(1 hunks)packages/app-store/apps.keys-schemas.generated.ts(2 hunks)packages/app-store/apps.metadata.generated.ts(2 hunks)packages/app-store/apps.schemas.generated.ts(2 hunks)packages/app-store/apps.server.generated.ts(1 hunks)packages/app-store/calendar.services.generated.ts(1 hunks)packages/app-store/nostrcalendar/DESCRIPTION.md(1 hunks)packages/app-store/nostrcalendar/_metadata.ts(1 hunks)packages/app-store/nostrcalendar/api/add.ts(1 hunks)packages/app-store/nostrcalendar/api/index.ts(1 hunks)packages/app-store/nostrcalendar/config.json(1 hunks)packages/app-store/nostrcalendar/index.ts(1 hunks)packages/app-store/nostrcalendar/lib/BunkerManager.ts(1 hunks)packages/app-store/nostrcalendar/lib/CalendarService.ts(1 hunks)packages/app-store/nostrcalendar/lib/NostrClient.ts(1 hunks)packages/app-store/nostrcalendar/lib/index.ts(1 hunks)packages/app-store/nostrcalendar/lib/types.ts(1 hunks)packages/app-store/nostrcalendar/package.json(1 hunks)packages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx(1 hunks)packages/app-store/nostrcalendar/zod.ts(1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/app-store/nostrcalendar/index.tspackages/app-store/nostrcalendar/api/index.tspackages/app-store/apps.metadata.generated.tspackages/app-store/apps.keys-schemas.generated.tspackages/app-store/nostrcalendar/lib/index.tspackages/app-store/apps.schemas.generated.tspackages/app-store/nostrcalendar/zod.tspackages/app-store/calendar.services.generated.tspackages/app-store/nostrcalendar/_metadata.tspackages/app-store/nostrcalendar/lib/CalendarService.tspackages/app-store/nostrcalendar/lib/NostrClient.tspackages/app-store/nostrcalendar/api/add.tspackages/app-store/nostrcalendar/lib/BunkerManager.tspackages/app-store/apps.server.generated.tspackages/app-store/nostrcalendar/lib/types.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/app-store/nostrcalendar/index.tspackages/app-store/nostrcalendar/api/index.tsapps/web/components/apps/AppSetupPage.tsxpackages/app-store/apps.metadata.generated.tspackages/app-store/apps.keys-schemas.generated.tspackages/app-store/nostrcalendar/lib/index.tspackages/app-store/apps.schemas.generated.tspackages/app-store/nostrcalendar/zod.tspackages/app-store/_pages/setup/_getServerSideProps.tsxpackages/app-store/calendar.services.generated.tspackages/app-store/nostrcalendar/_metadata.tspackages/app-store/nostrcalendar/lib/CalendarService.tsapps/web/components/apps/nostrcalendar/Setup.tsxpackages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsxpackages/app-store/nostrcalendar/lib/NostrClient.tspackages/app-store/nostrcalendar/api/add.tspackages/app-store/nostrcalendar/lib/BunkerManager.tspackages/app-store/apps.server.generated.tspackages/app-store/nostrcalendar/lib/types.ts
**/*.{ts,tsx,js,jsx}
⚙️ CodeRabbit configuration file
Flag default exports and encourage named exports. Named exports provide better tree-shaking, easier refactoring, and clearer imports. Exempt main components like pages, layouts, and components that serve as the primary export of a module.
Files:
packages/app-store/nostrcalendar/index.tspackages/app-store/nostrcalendar/api/index.tsapps/web/components/apps/AppSetupPage.tsxpackages/app-store/apps.metadata.generated.tspackages/app-store/apps.keys-schemas.generated.tspackages/app-store/nostrcalendar/lib/index.tspackages/app-store/apps.schemas.generated.tspackages/app-store/nostrcalendar/zod.tspackages/app-store/_pages/setup/_getServerSideProps.tsxpackages/app-store/calendar.services.generated.tspackages/app-store/nostrcalendar/_metadata.tspackages/app-store/nostrcalendar/lib/CalendarService.tsapps/web/components/apps/nostrcalendar/Setup.tsxpackages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsxpackages/app-store/nostrcalendar/lib/NostrClient.tspackages/app-store/nostrcalendar/api/add.tspackages/app-store/nostrcalendar/lib/BunkerManager.tspackages/app-store/apps.server.generated.tspackages/app-store/nostrcalendar/lib/types.ts
**/*.tsx
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Always use
t()for text localization in frontend code; direct text embedding should trigger a warning
Files:
apps/web/components/apps/AppSetupPage.tsxpackages/app-store/_pages/setup/_getServerSideProps.tsxapps/web/components/apps/nostrcalendar/Setup.tsxpackages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx
**/*Service.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Service files must include
Servicesuffix, use PascalCase matching exported class, and avoid generic names (e.g.,MembershipService.ts)
Files:
packages/app-store/nostrcalendar/lib/CalendarService.ts
🧠 Learnings (1)
📚 Learning: 2025-08-05T12:04:29.037Z
Learnt from: din-prajapati
PR: calcom/cal.com#21854
File: packages/app-store/office365calendar/__tests__/unit_tests/SubscriptionManager.test.ts:0-0
Timestamp: 2025-08-05T12:04:29.037Z
Learning: In packages/app-store/office365calendar/lib/CalendarService.ts, the fetcher method in Office365CalendarService class is public, not private. It was specifically changed from private to public in this PR to support proper testing and external access patterns.
Applied to files:
packages/app-store/nostrcalendar/lib/CalendarService.ts
🧬 Code graph analysis (6)
packages/app-store/nostrcalendar/lib/CalendarService.ts (5)
packages/app-store/nostrcalendar/lib/NostrClient.ts (1)
NostrClient(20-1068)packages/app-store/nostrcalendar/lib/BunkerManager.ts (1)
BunkerManager(26-165)packages/platform/libraries/index.ts (1)
symmetricDecrypt(80-80)packages/types/Calendar.d.ts (3)
NewCalendarEventType(71-84)CalendarEvent(163-226)IntegrationCalendar(245-254)packages/lib/CalEventParser.ts (1)
getLocation(123-132)
apps/web/components/apps/nostrcalendar/Setup.tsx (2)
packages/ui/components/form/inputs/TextField.tsx (1)
TextField(234-236)packages/ui/components/button/Button.tsx (1)
Button(221-349)
packages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx (2)
packages/app-store/_pages/setup/_getServerSideProps.tsx (1)
getServerSideProps(13-26)packages/features/auth/lib/getServerSession.ts (1)
getServerSession(32-136)
packages/app-store/nostrcalendar/lib/NostrClient.ts (1)
packages/app-store/nostrcalendar/lib/types.ts (3)
AuthType(6-6)CalendarEventKind(15-15)ParsedCalendarEvent(38-47)
packages/app-store/nostrcalendar/api/add.ts (5)
packages/app-store/nostrcalendar/lib/BunkerManager.ts (1)
BunkerManager(26-165)packages/platform/libraries/index.ts (1)
symmetricEncrypt(80-80)packages/app-store/nostrcalendar/lib/NostrClient.ts (2)
NostrClient(20-1068)getPublicKey(56-77)packages/lib/server/defaultHandler.ts (1)
defaultHandler(8-17)packages/lib/server/defaultResponder.ts (1)
defaultResponder(9-47)
packages/app-store/nostrcalendar/lib/BunkerManager.ts (1)
packages/app-store/nostrcalendar/lib/types.ts (1)
BunkerConnection(9-12)
⏰ Context from checks skipped due to timeout of 180000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (6)
apps/web/components/apps/AppSetupPage.tsx (1)
20-20: LGTM!The nostrcalendar entry follows the established pattern for app setup mapping and is correctly positioned in the AppSetupMap object.
packages/app-store/apps.keys-schemas.generated.ts (2)
30-30: LGTM!The import statement follows the established pattern for auto-generated schema imports.
82-82: LGTM!The appKeysSchemas entry is correctly positioned and follows the pattern of other calendar integrations.
packages/app-store/apps.server.generated.ts (1)
63-63: LGTM!The API handler registration follows the established pattern and is correctly positioned alphabetically.
packages/app-store/_pages/setup/_getServerSideProps.tsx (1)
10-10: LGTM!The nostrcalendar setup page mapping is correctly added and follows the established pattern for apps requiring custom server-side setup logic.
packages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx (1)
5-21: LGTM!The authentication guard correctly redirects unauthenticated users to login and returns empty props for authenticated users. The implementation follows Next.js SSR patterns.
| export const metadata = { | ||
| name: "Nostr", | ||
| description: _package.description, | ||
| installed: true, | ||
| type: "nostr_calendar", | ||
| title: "Nostr Calendar", | ||
| variant: "calendar", | ||
| category: "calendar", | ||
| categories: ["calendar"], | ||
| logo: "icon.svg", | ||
| publisher: "NostrCal.com", | ||
| slug: "nostr", | ||
| url: "https://nostr.com", | ||
| email: "hello@nostrcal.com", | ||
| dirName: "nostrcalendar", | ||
| isOAuth: false, | ||
| } as AppMeta; |
There was a problem hiding this comment.
CRITICAL: Fix slug inconsistency.
There is a critical inconsistency in the slug definition:
- config.json (Line 4):
"slug": "nostrcalendar" - _metadata.ts (Line 16):
slug: "nostr"
This mismatch will cause the app store to fail resolving the integration properly. The slug must be identical across all configuration files.
Additionally, there's a URL inconsistency:
- config.json (Line 8):
"url": "https://nostrcal.com" - _metadata.ts (Line 17):
url: "https://nostr.com"
Apply this diff to align the slug with config.json and fix the URL:
export const metadata = {
name: "Nostr",
description: _package.description,
installed: true,
type: "nostr_calendar",
title: "Nostr Calendar",
variant: "calendar",
category: "calendar",
categories: ["calendar"],
logo: "icon.svg",
publisher: "NostrCal.com",
- slug: "nostr",
- url: "https://nostr.com",
+ slug: "nostrcalendar",
+ url: "https://nostrcal.com",
email: "hello@nostrcal.com",
dirName: "nostrcalendar",
isOAuth: false,
} as AppMeta;🤖 Prompt for AI Agents
In packages/app-store/nostrcalendar/_metadata.ts around lines 5 to 21, the slug
and url values are inconsistent with config.json (slug should be "nostrcalendar"
and url should be "https://nostrcal.com"); update the slug from "nostr" to
"nostrcalendar" and the url from "https://nostr.com" to "https://nostrcal.com"
so both files match exactly, and verify the same values are used across any
other related config files.
| try { | ||
| if (authMethod === "bunker") { | ||
| // Bunker-based authentication | ||
| if (!bunkerUri || typeof bunkerUri !== "string") { | ||
| throw new HttpError({ statusCode: 400, message: "bunkerUri is required for bunker auth" }); | ||
| } | ||
|
|
||
| log.info("Setting up bunker authentication", { userId: loggedInUser.id }); | ||
|
|
||
| // Validate bunker URI format | ||
| if (!BunkerManager.isValidBunkerUri(bunkerUri)) { | ||
| throw new Error("Invalid bunker URI format. Expected bunker:// or name@domain.com"); | ||
| } | ||
|
|
||
| // Connect to bunker | ||
| const { bunker, clientSecret } = await BunkerManager.connectFromUri(bunkerUri); | ||
|
|
||
| // Get public key from bunker | ||
| const publicKey = await bunker.getPublicKey(); | ||
| const npub = nip19.npubEncode(publicKey); | ||
|
|
||
| log.info("Connected to bunker successfully", { | ||
| userId: loggedInUser.id, | ||
| npub: npub.substring(0, 12) + "...", | ||
| }); | ||
|
|
||
| // Encrypt client secret for storage | ||
| const encryptedClientSecret = symmetricEncrypt(Buffer.from(clientSecret).toString("hex"), decodedKey); | ||
|
|
||
| // Query user's kind 0 metadata to get display name | ||
| const nostrClient = new NostrClient({ bunker }); | ||
| const displayName = await nostrClient.queryUserMetadata(); | ||
|
|
||
| // Store credential | ||
| const data = { | ||
| type: appConfig.type, | ||
| key: { | ||
| authType: "bunker" as const, | ||
| bunkerUri, | ||
| localClientSecret: encryptedClientSecret, | ||
| npub, | ||
| ...(displayName && { displayName }), | ||
| }, | ||
| userId: loggedInUser.id, | ||
| teamId: null, | ||
| appId: appConfig.slug, | ||
| invalid: false, | ||
| }; | ||
|
|
||
| await prisma.credential.create({ data }); | ||
|
|
||
| // Clean up bunker connection | ||
| await bunker.close(); | ||
|
|
||
| log.info("Bunker credential created successfully", { userId: loggedInUser.id, npub }); | ||
|
|
||
| return res.status(200).json({ | ||
| url: getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug }), | ||
| }); | ||
| } else { | ||
| // nsec-based authentication (existing flow) | ||
| if (!nsec || typeof nsec !== "string") { | ||
| throw new HttpError({ statusCode: 400, message: "nsec key is required for nsec auth" }); | ||
| } | ||
|
|
||
| log.info("Setting up nsec authentication", { userId: loggedInUser.id }); | ||
|
|
||
| // Decode nsec to validate and get public key | ||
| const decoded = nip19.decode(nsec); | ||
| if (decoded.type !== "nsec") { | ||
| throw new Error("Invalid nsec key format"); | ||
| } | ||
|
|
||
| const secretKey = decoded.data as Uint8Array; | ||
| const publicKey = getPublicKey(secretKey); | ||
| const npub = nip19.npubEncode(publicKey); | ||
|
|
||
| // Encrypt the nsec before storing | ||
| const encryptedNsec = symmetricEncrypt(nsec, decodedKey); | ||
|
|
||
| // Query user's kind 0 metadata to get display name | ||
| const nostrClient = new NostrClient({ nsec }); | ||
| const displayName = await nostrClient.queryUserMetadata(publicKey); | ||
| nostrClient.close(); | ||
|
|
||
| // Store credential (relays will be discovered from kind 10002) | ||
| const data = { | ||
| type: appConfig.type, | ||
| key: { | ||
| authType: "nsec" as const, | ||
| nsec: encryptedNsec, | ||
| npub, | ||
| ...(displayName && { displayName }), | ||
| }, | ||
| userId: loggedInUser.id, | ||
| teamId: null, | ||
| appId: appConfig.slug, | ||
| invalid: false, | ||
| }; | ||
|
|
||
| await prisma.credential.create({ data }); | ||
|
|
||
| log.info("Nsec credential created successfully", { userId: loggedInUser.id, npub }); | ||
|
|
||
| return res.status(200).json({ | ||
| url: getInstalledAppPath({ variant: appConfig.variant, slug: appConfig.slug }), | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| const error = e as Error; | ||
| log.error("Could not add Nostr account", error); | ||
| return res.status(500).json({ | ||
| message: error.message || "Could not add Nostr account. Please verify your credentials are correct.", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Always close bunker/Nostr client resources on failure.
In the bunker branch we only call bunker.close() after the credential write. If queryUserMetadata or prisma.credential.create throws, we skip both bunker.close() and nostrClient.close(), leaving sockets open. Wrap those operations in try/finally so both the signer and client pools are shut down on every path.
🤖 Prompt for AI Agents
In packages/app-store/nostrcalendar/api/add.ts around lines 44 to 158, the
bunker and Nostr client resources are only closed on the success path so if
queryUserMetadata or prisma.credential.create throws the bunker (and
nostrClient) connections remain open; wrap the work that uses bunker and the
related NostrClient in a try/finally where you always call bunker.close() in the
finally (and similarly wrap the NostrClient usage in both branches so
nostrClient.close() runs in finally), and protect the close calls with their own
try/catch so close failures don’t mask the original error.
| } catch (e) { | ||
| const error = e as Error; | ||
| log.error("Could not add Nostr account", error); | ||
| return res.status(500).json({ | ||
| message: error.message || "Could not add Nostr account. Please verify your credentials are correct.", | ||
| }); |
There was a problem hiding this comment.
Propagate HttpError status codes instead of hard‑coding 500.
HttpError thrown above (e.g., invalid authMethod, missing bunkerUri) is swallowed here and re-emitted as a 500. The client loses the 4xx signal and the UI shows a server failure instead of actionable validation feedback. Please preserve the original status (and only fall back to 500 for unexpected errors).
🤖 Prompt for AI Agents
In packages/app-store/nostrcalendar/api/add.ts around lines 152-157, the catch
block currently always returns a 500 and swallows HttpError status codes; change
it to detect if the caught error is an instance of HttpError (or has a .status
property), log the error and its status, and return res.status(err.status ||
500).json({ message: err.message || "Could not add Nostr account. Please verify
your credentials are correct." }); so 4xx validation errors are preserved while
unexpected errors still fall back to 500.
| { | ||
| "/*": "Don't modify slug - If required, do it using cli edit command", | ||
| "name": "nostrcalendar", | ||
| "slug": "nostrcalendar", | ||
| "type": "nostr_calendar", | ||
| "title": "Nostr Calendar", | ||
| "logo": "icon.svg", | ||
| "url": "https://nostrcal.com", | ||
| "variant": "calendar", | ||
| "categories": ["calendar"], | ||
| "publisher": "NostrCal.com", | ||
| "email": "hello@nostrcal.com", | ||
| "description": "Sync your calendar with Nostr (NIP-52) events", | ||
| "isTemplate": false, | ||
| "__createdUsingCli": true, | ||
| "__template": "basic" | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
Verify type field and slug consistency.
The configuration appears complete, but please verify:
-
Type field: The
typeis set to"nostr_calendar", which differs from other calendar integrations that typically use"calendar"as the type. Ensure this is intentional and supported by the app store type system. -
Slug consistency: The config uses
"nostrcalendar"as the slug, but the metadata file (packages/app-store/nostrcalendar/_metadata.ts) usesslug: "nostr". This inconsistency could cause routing or lookup issues.
Run the following script to check slug usage across the codebase and verify type field patterns:
🏁 Script executed:
#!/bin/bash
# Check slug references and type field patterns
echo "=== Checking nostrcalendar slug references ==="
rg -n "nostrcalendar|nostr\"" --type=json --type=ts --type=tsx
echo ""
echo "=== Checking calendar app type fields ==="
rg -n '"type":\s*"[^"]*calendar[^"]*"' packages/app-store/*/config.jsonLength of output: 754
🏁 Script executed:
#!/bin/bash
rg -nP 'slug\s*:' -C2 packages/app-store/nostrcalendar/_metadata.tsLength of output: 139
Align slug in config.json with metadata.ts
The slug in packages/app-store/nostrcalendar/config.json is "nostrcalendar" but in packages/app-store/nostrcalendar/_metadata.ts it’s "nostr". This mismatch will break routing/lookups—make them identical.
🤖 Prompt for AI Agents
In packages/app-store/nostrcalendar/config.json lines 1-17, the "slug" value is
"nostrcalendar" which mismatches packages/app-store/nostrcalendar/_metadata.ts
(which uses "nostr"); update the "slug" in config.json to exactly match the
metadata value ("nostr") so routing/lookups align, save the file, and run any
local dev build or tests to verify there are no remaining references to the old
slug.
| log.info("Nostr events published successfully", { eventId, blockId }); | ||
|
|
||
| return { | ||
| uid: "", | ||
| id: eventId, | ||
| type: this.integrationName, | ||
| password: "", | ||
| url: "", | ||
| additionalInfo: { | ||
| blockId: blockId, | ||
| }, | ||
| }; | ||
| } catch (error) { | ||
| log.error("Error creating Nostr calendar event", error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Update an existing calendar event | ||
| */ | ||
| async updateEvent( | ||
| uid: string, | ||
| event: CalendarServiceEvent, | ||
| _externalCalendarId: string | ||
| ): Promise<NewCalendarEventType> { | ||
| log.debug("Updating Nostr calendar event", { uid }); | ||
|
|
||
| // Nostr doesn't support updates - we need to delete and recreate | ||
| try { | ||
| const client = await this.getNostrClient(); | ||
|
|
||
| // Delete old event | ||
| await client.deleteEvent(uid, "Event updated"); | ||
|
|
||
| // Create new event | ||
| return await this.createEvent(event, this.credential.id); | ||
| } catch (error) { | ||
| log.error("Error updating Nostr calendar event", error); | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Delete a calendar event and its availability block | ||
| */ | ||
| async deleteEvent(uid: string, event: CalendarEvent, _externalCalendarId?: string | null): Promise<void> { | ||
| log.info("Deleting Nostr calendar event", { | ||
| uid, | ||
| startTime: event.startTime, | ||
| endTime: event.endTime, | ||
| }); | ||
|
|
||
| try { | ||
| const client = await this.getNostrClient(); | ||
|
|
||
| // Delete the main calendar event (kind 31923) | ||
| await client.deleteEvent(uid, "Event cancelled"); | ||
|
|
||
| // Delete the associated availability block(s) (kind 31927) by time | ||
| await client.deleteAvailabilityBlocksByTime(new Date(event.startTime), new Date(event.endTime)); | ||
|
|
||
| log.info("Nostr calendar event and availability block deleted successfully"); | ||
| } catch (error) { | ||
| log.error("Error deleting Nostr calendar event", error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Deleting/updating fails for bunker-auth events
When NostrClient.createCalendarEvent runs under bunker auth it returns a generated UUID placeholder (no actual Nostr event id). We store that placeholder in bookings, then updateEvent/deleteEvent publish NIP‑09 deletions using the same placeholder. Those deletions never match the real event on relays, so bunker users can’t update or cancel bookings—old events and availability blocks linger. Please return the real event id (e.g. have createGiftWrapWithBunker surface the computed rumor id) or switch the mutation logic to resolve the true ids before issuing deletions.
| // For bunker auth, we need to decrypt the wrap manually | ||
| // This requires implementing manual unwrapping with bunker.nip44Decrypt | ||
| log.warn("Bunker-based unwrapping not yet implemented for received events"); | ||
| } |
There was a problem hiding this comment.
Bunker flow can’t read or manage its own private events.
For bunker auth you log “not yet implemented” and skip decrypting wraps, which means queryPrivateEvents returns nothing, conflict detection can’t see private bookings, and createCalendarEvent falls back to a random tempId that won’t match the real rumor (so later deletes/updates fail). Please implement unwrapping with the bunker signer (nip44Decrypt the seal content, reconstruct the rumor, verify the signature, and reuse the real event id) and return that real id from createCalendarEvent.
Also applies to: 905-915
🤖 Prompt for AI Agents
In packages/app-store/nostrcalendar/lib/NostrClient.ts around lines 614-617 (and
similarly 905-915), the code currently logs "Bunker-based unwrapping not yet
implemented" and skips decrypting wrapped/ sealed private events, causing
queryPrivateEvents to return nothing and createCalendarEvent to use a tempId
that won't match the real rumor; implement bunker unwrapping by calling the
bunker signer nip44Decrypt on the seal payload, parse the decrypted content to
reconstruct the original rumor/event payload, verify the rumor signature with
the event's pubkey, extract and reuse the real event id (replace the tempId),
and ensure the unwrapped event is returned by queryPrivateEvents and that
createCalendarEvent returns the real id so future updates/deletes match; handle
decryption/signature errors by logging and skipping only that event, not the
whole flow.
| publishPromises.push( | ||
| Promise.any(this.pool.publish(targetRelays, wrap)) | ||
| .then(() => { | ||
| log.info(`✓ Gift wrap ${i + 1} delivered successfully to ${recipientPubkey.slice(0, 8)}`); | ||
| }) | ||
| .catch((err) => { | ||
| log.warn(`✗ Failed to deliver gift wrap ${i + 1} to ${recipientPubkey.slice(0, 8)}`, err); | ||
| }) | ||
| ); |
There was a problem hiding this comment.
Fix misuse of Promise.any for relay publishes.
Promise.any expects an iterable; passing a single promise throws immediately, so every publish path (private wraps, public events, availability blocks, deletions) currently fails. Call this.pool.publish(...) directly (or wrap it in an array) before chaining the success/error handlers.
- publishPromises.push(
- Promise.any(this.pool.publish(targetRelays, wrap))
- .then(() => {
+ publishPromises.push(
+ this.pool
+ .publish(targetRelays, wrap)
+ .then(() => {
@@
- await Promise.any(this.pool.publish(relays, signedEvent));
+ await this.pool.publish(relays, signedEvent);Apply the same adjustment to the availability and deletion publishers so they no longer throw synchronously.
Also applies to: 933-937, 973-976, 1004-1007
🤖 Prompt for AI Agents
In packages/app-store/nostrcalendar/lib/NostrClient.ts around lines 888–896 (and
similarly at 933–937, 973–976, 1004–1007), the code incorrectly passes a single
Promise to Promise.any which throws synchronously; replace
Promise.any(this.pool.publish(...)) with either calling this.pool.publish(...)
directly and chaining .then/.catch, or wrap the publish call in an array like
Promise.any([this.pool.publish(...)]) so it receives an iterable; apply the same
change to the availability and deletion publish sites and keep the existing
.then and .catch handlers intact.
pallava-joshi
left a comment
There was a problem hiding this comment.
https://jumpshare.com/share/WpQHlfKG9GW2CJBA5Bvz
On testing this, I ran into an issue - any idea what might be causing it?
Off first glance it looks like an issue with communicating to the nostr relays. Was the npub used just for testing with any nostr history for relay preferences before testing? My first thought was around relay connections not finding the npub's kind 10002/10050 relay lists via any events and potentially hanging there. I only tested a fresh npub once though I didn't have issues on that test. |
|
Yeah, it’s a fresh nsec account with no existing relay history. Could that be what’s causing the hang you mentioned? also please a link a complete testing video on your end |
|
hey - quick follow up did you gave it a try again? i'm still facing the same issue |
|
This PR is being marked as stale due to inactivity. |
Adds Nostr calendar integration supporting NIP-52 calendar events.
Features:
What does this PR do?
Adds Nostr calendar integration to Cal.com's app store, enabling users to sync their Cal.com bookings with Nostr relays using the NIP-52 calendar events specification.
Key Features:
Note: This is a new feature contribution. No existing issue - this PR introduces the Nostr
calendar integration to Cal.com.
Video Demo (if applicable):
https://relay.patrickulrich.com/e222d48bbaa9028f63918e1bf7d65c1c009aeefb33f740590b8afbb40b442dbd.mov
Image Demo (if applicable):
Setup page showing authentication options:
Mandatory Tasks (DO NOT REMOVE)
How should this be tested?
Environment Variables Required:
CALENDSO_ENCRYPTION_KEY=
Minimal Test Data:
- A Nostr bunker URI (e.g., from nsec.app or Amber)
- Format: bunker://pubkey?relay=wss://relay.example.com or user@domain.com
- A valid Nostr nsec private key (starts with nsec1...)
Testing Steps:
Setup:
Bunker Method (Recommended):
Nsec Method:
Calendar Functionality:
Expected Output:
Verification:
Use a Nostr client (like nostrcal.com) to verify events are published correctly to your relays.