Skip to content

Comments

feat: add nostr calendar integration#24266

Open
patrickulrich wants to merge 1 commit intocalcom:mainfrom
patrickulrich:nostrcal
Open

feat: add nostr calendar integration#24266
patrickulrich wants to merge 1 commit intocalcom:mainfrom
patrickulrich:nostrcal

Conversation

@patrickulrich
Copy link

Adds Nostr calendar integration supporting NIP-52 calendar events.

Features:

  • Authentication: Supports both Bunker (NIP-46 remote signer) and direct nsec key
  • Privacy: Creates private NIP-59 gift-wrapped calendar events with public availability blocks
  • Conflict Detection: Checks existing Nostr calendar events to prevent double-bookings
  • Auto-Discovery: Automatically discovers user relay list from kind 10002 metadata

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:

  • Two authentication methods: Bunker (NIP-46 remote signer) and direct nsec key
  • Creates private NIP-59 gift-wrapped calendar events for privacy
  • Publishes public availability blocks (kind 31927) to prevent double-bookings
  • Automatic relay discovery from user's kind 10002 metadata
  • Implements NIPs: 9, 44, 46, 52, 59

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:

  • Bunker/Nsec authentication method setup: packages/app-store/nostrcalendar/static/2.jpeg

Mandatory Tasks (DO NOT REMOVE)

  • I have self-reviewed the code (A decent size PR without self-review might be rejected).
  • I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. If N/A, write N/A here and check the checkbox.
  • I confirm automated tests are in place that prove my fix is effective or that my feature works.

How should this be tested?

Environment Variables Required:

CALENDSO_ENCRYPTION_KEY=

Minimal Test Data:

  1. For Bunker Authentication:
    - A Nostr bunker URI (e.g., from nsec.app or Amber)
    - Format: bunker://pubkey?relay=wss://relay.example.com or user@domain.com
  2. For Nsec Authentication:
    - A valid Nostr nsec private key (starts with nsec1...)

Testing Steps:

Setup:

  1. Navigate to Cal.com apps page
  2. Search for "Nostrcalendar"
  3. Click "Install"

Bunker Method (Recommended):

  1. Select "Bunker Connection" authentication
  2. Enter bunker URI
  3. Approve connection in your bunker app
  4. Verify calendar appears in connected apps

Nsec Method:

  1. Select "Private Key (nsec)" authentication
  2. Enter nsec key
  3. Submit form
  4. Verify calendar appears in connected apps

Calendar Functionality:

  1. Create a booking
  2. Verify private calendar event is created on Nostr relays
  3. Verify public availability block is created
  4. Check availability - should show as busy
  5. Update booking - event should update
  6. Cancel booking - event should be deleted

Expected Output:

  • Calendar events published to Nostr relays (kind 31923)
  • Availability blocks created (kind 31927)
  • Events encrypted with NIP-59 gift wrap
  • User's relay list auto-discovered from kind 10002

Verification:

Use a Nostr client (like nostrcal.com) to verify events are published correctly to your relays.

  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 patrickulrich requested a review from a team as a code owner October 4, 2025 04:08
@vercel
Copy link

vercel bot commented Oct 4, 2025

@patrickulrich is attempting to deploy a commit to the cal Team on Vercel.

A member of the Team first needs to authorize it.

@CLAassistant
Copy link

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@graphite-app graphite-app bot requested a review from a team October 4, 2025 04:08
@graphite-app graphite-app bot added the community Created by Linear-GitHub Sync label Oct 4, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 4, 2025

Walkthrough

This 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)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The provided title clearly summarizes the main change by stating the addition of the Nostr calendar integration and follows conventional commit style without unnecessary detail.
Description Check ✅ Passed The description thoroughly outlines the features, authentication methods, privacy guarantees, and testing steps for the Nostr calendar integration and directly relates to the changes introduced in the pull request.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Warning

Review ran into problems

🔥 Problems

Errors were encountered while retrieving linked issues.

Errors (3)
  • NIP-52: Entity not found: Issue - Could not find referenced Issue.
  • NIP-46: Entity not found: Issue - Could not find referenced Issue.
  • NIP-59: Entity not found: Issue - Could not find referenced Issue.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dosubot dosubot bot added app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar ✨ feature New feature or request labels Oct 4, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_KEY environment 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 store
apps/web/components/apps/nostrcalendar/Setup.tsx (1)

47-158: Localize user-facing copy with t()

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 through t() (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.

📥 Commits

Reviewing files that changed from the base of the PR and between 7cd8dbf and d270c8c.

⛔ Files ignored due to path filters (5)
  • packages/app-store/nostrcalendar/static/1.jpeg is excluded by !**/*.jpeg
  • packages/app-store/nostrcalendar/static/2.jpeg is excluded by !**/*.jpeg
  • packages/app-store/nostrcalendar/static/3.jpeg is excluded by !**/*.jpeg
  • packages/app-store/nostrcalendar/static/icon.svg is excluded by !**/*.svg
  • yarn.lock is 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 use include, always use select
Ensure the credential.key field is never returned from tRPC endpoints or APIs

Files:

  • packages/app-store/nostrcalendar/index.ts
  • packages/app-store/nostrcalendar/api/index.ts
  • packages/app-store/apps.metadata.generated.ts
  • packages/app-store/apps.keys-schemas.generated.ts
  • packages/app-store/nostrcalendar/lib/index.ts
  • packages/app-store/apps.schemas.generated.ts
  • packages/app-store/nostrcalendar/zod.ts
  • packages/app-store/calendar.services.generated.ts
  • packages/app-store/nostrcalendar/_metadata.ts
  • packages/app-store/nostrcalendar/lib/CalendarService.ts
  • packages/app-store/nostrcalendar/lib/NostrClient.ts
  • packages/app-store/nostrcalendar/api/add.ts
  • packages/app-store/nostrcalendar/lib/BunkerManager.ts
  • packages/app-store/apps.server.generated.ts
  • packages/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.ts
  • packages/app-store/nostrcalendar/api/index.ts
  • apps/web/components/apps/AppSetupPage.tsx
  • packages/app-store/apps.metadata.generated.ts
  • packages/app-store/apps.keys-schemas.generated.ts
  • packages/app-store/nostrcalendar/lib/index.ts
  • packages/app-store/apps.schemas.generated.ts
  • packages/app-store/nostrcalendar/zod.ts
  • packages/app-store/_pages/setup/_getServerSideProps.tsx
  • packages/app-store/calendar.services.generated.ts
  • packages/app-store/nostrcalendar/_metadata.ts
  • packages/app-store/nostrcalendar/lib/CalendarService.ts
  • apps/web/components/apps/nostrcalendar/Setup.tsx
  • packages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx
  • packages/app-store/nostrcalendar/lib/NostrClient.ts
  • packages/app-store/nostrcalendar/api/add.ts
  • packages/app-store/nostrcalendar/lib/BunkerManager.ts
  • packages/app-store/apps.server.generated.ts
  • packages/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.ts
  • packages/app-store/nostrcalendar/api/index.ts
  • apps/web/components/apps/AppSetupPage.tsx
  • packages/app-store/apps.metadata.generated.ts
  • packages/app-store/apps.keys-schemas.generated.ts
  • packages/app-store/nostrcalendar/lib/index.ts
  • packages/app-store/apps.schemas.generated.ts
  • packages/app-store/nostrcalendar/zod.ts
  • packages/app-store/_pages/setup/_getServerSideProps.tsx
  • packages/app-store/calendar.services.generated.ts
  • packages/app-store/nostrcalendar/_metadata.ts
  • packages/app-store/nostrcalendar/lib/CalendarService.ts
  • apps/web/components/apps/nostrcalendar/Setup.tsx
  • packages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx
  • packages/app-store/nostrcalendar/lib/NostrClient.ts
  • packages/app-store/nostrcalendar/api/add.ts
  • packages/app-store/nostrcalendar/lib/BunkerManager.ts
  • packages/app-store/apps.server.generated.ts
  • packages/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.tsx
  • packages/app-store/_pages/setup/_getServerSideProps.tsx
  • apps/web/components/apps/nostrcalendar/Setup.tsx
  • packages/app-store/nostrcalendar/pages/setup/_getServerSideProps.tsx
**/*Service.ts

📄 CodeRabbit inference engine (.cursor/rules/review.mdc)

Service files must include Service suffix, 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.

Comment on lines +5 to +21
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +44 to +158
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.",
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +152 to +157
} 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.",
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +1 to +17
{
"/*": "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"
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify type field and slug consistency.

The configuration appears complete, but please verify:

  1. Type field: The type is 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.

  2. Slug consistency: The config uses "nostrcalendar" as the slug, but the metadata file (packages/app-store/nostrcalendar/_metadata.ts) uses slug: "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.json

Length of output: 754


🏁 Script executed:

#!/bin/bash
rg -nP 'slug\s*:' -C2 packages/app-store/nostrcalendar/_metadata.ts

Length 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.

Comment on lines +120 to +187
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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +614 to +617
// 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");
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Comment on lines +888 to +896
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);
})
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

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.

Copy link
Contributor

@pallava-joshi pallava-joshi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://jumpshare.com/share/WpQHlfKG9GW2CJBA5Bvz

On testing this, I ran into an issue - any idea what might be causing it?

@pallava-joshi pallava-joshi marked this pull request as draft October 6, 2025 04:50
@patrickulrich
Copy link
Author

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.

@pallava-joshi
Copy link
Contributor

pallava-joshi commented Oct 7, 2025

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

@pallava-joshi
Copy link
Contributor

hey - quick follow up did you gave it a try again? i'm still facing the same issue

@pallava-joshi pallava-joshi marked this pull request as ready for review October 10, 2025 09:36
@github-actions
Copy link
Contributor

This PR is being marked as stale due to inactivity.

@github-actions github-actions bot added the Stale label Oct 25, 2025
@devin-ai-integration devin-ai-integration bot added the Medium priority Created by Linear-GitHub Sync label Jan 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app-store area: app store, apps, calendar integrations, google calendar, outlook, lark, apple calendar community Created by Linear-GitHub Sync ✨ feature New feature or request Medium priority Created by Linear-GitHub Sync size/XXL Stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants