···11export * as AppBskyActorProfile from "./types/app/bsky/actor/profile.js";
22export * as CommunityLexiconCalendarEvent from "./types/community/lexicon/calendar/event.js";
33-export * as CommunityLexiconCalendarEventGetRecord from "./types/community/lexicon/calendar/event/getRecord.js";
44-export * as CommunityLexiconCalendarEventListRecords from "./types/community/lexicon/calendar/event/listRecords.js";
53export * as CommunityLexiconCalendarRsvp from "./types/community/lexicon/calendar/rsvp.js";
66-export * as CommunityLexiconCalendarRsvpGetRecord from "./types/community/lexicon/calendar/rsvp/getRecord.js";
77-export * as CommunityLexiconCalendarRsvpListRecords from "./types/community/lexicon/calendar/rsvp/listRecords.js";
84export * as CommunityLexiconLocationAddress from "./types/community/lexicon/location/address.js";
95export * as CommunityLexiconLocationFsq from "./types/community/lexicon/location/fsq.js";
106export * as CommunityLexiconLocationGeo from "./types/community/lexicon/location/geo.js";
117export * as CommunityLexiconLocationHthree from "./types/community/lexicon/location/hthree.js";
88+export * as RsvpAtmoEventGetRecord from "./types/rsvp/atmo/event/getRecord.js";
99+export * as RsvpAtmoEventListRecords from "./types/rsvp/atmo/event/listRecords.js";
1210export * as RsvpAtmoGetCursor from "./types/rsvp/atmo/getCursor.js";
1311export * as RsvpAtmoGetOverview from "./types/rsvp/atmo/getOverview.js";
1412export * as RsvpAtmoGetProfile from "./types/rsvp/atmo/getProfile.js";
1513export * as RsvpAtmoNotifyOfUpdate from "./types/rsvp/atmo/notifyOfUpdate.js";
1414+export * as RsvpAtmoRsvpGetRecord from "./types/rsvp/atmo/rsvp/getRecord.js";
1515+export * as RsvpAtmoRsvpListRecords from "./types/rsvp/atmo/rsvp/listRecords.js";
1616+export * as RsvpAtmoSpaceAdminAddMember from "./types/rsvp/atmo/space/admin/addMember.js";
1717+export * as RsvpAtmoSpaceAdminCreateSpace from "./types/rsvp/atmo/space/admin/createSpace.js";
1818+export * as RsvpAtmoSpaceAdminRemoveMember from "./types/rsvp/atmo/space/admin/removeMember.js";
1919+export * as RsvpAtmoSpaceDefs from "./types/rsvp/atmo/space/defs.js";
2020+export * as RsvpAtmoSpaceDeleteRecord from "./types/rsvp/atmo/space/deleteRecord.js";
2121+export * as RsvpAtmoSpaceGetRecord from "./types/rsvp/atmo/space/getRecord.js";
2222+export * as RsvpAtmoSpaceGetSpace from "./types/rsvp/atmo/space/getSpace.js";
2323+export * as RsvpAtmoSpaceInviteCreate from "./types/rsvp/atmo/space/invite/create.js";
2424+export * as RsvpAtmoSpaceInviteList from "./types/rsvp/atmo/space/invite/list.js";
2525+export * as RsvpAtmoSpaceInviteRedeem from "./types/rsvp/atmo/space/invite/redeem.js";
2626+export * as RsvpAtmoSpaceInviteRevoke from "./types/rsvp/atmo/space/invite/revoke.js";
2727+export * as RsvpAtmoSpaceListMembers from "./types/rsvp/atmo/space/listMembers.js";
2828+export * as RsvpAtmoSpaceListRecords from "./types/rsvp/atmo/space/listRecords.js";
2929+export * as RsvpAtmoSpaceListSpaces from "./types/rsvp/atmo/space/listSpaces.js";
3030+export * as RsvpAtmoSpacePutRecord from "./types/rsvp/atmo/space/putRecord.js";
···159159}
160160161161function writeGeneratedService(hostname: string, tunnelUrl: string): void {
162162- const did = `did:web:${hostname}#${SERVICE_FRAGMENT}`;
162162+ // NOTE: plain DID, no fragment. PDSes reject fragments in getServiceAuth's `aud` param,
163163+ // and the library's middleware does strict string equality. The fragment is only used
164164+ // by PDSes to look up service entries in the DID doc for Atproto-Proxy routing — that
165165+ // concern is separate from JWT audience validation.
166166+ const did = `did:web:${hostname}`;
163167 const body =
164168 `/** Auto-generated by \`pnpm tunnel\`. Do not edit by hand.\n` +
165169 ` * When the tunnel is running, this file is rewritten with the tunnel's\n` +
+10-18
src/lib/atproto/settings.ts
···11import { dev } from '$app/environment';
22import { scope } from '@atcute/oauth-node-client';
3344-// writable collections
44+// writable collections — declared as a standalone scope because their NSIDs
55+// (`community.lexicon.*`) sit outside our namespace, so they can't go in
66+// `rsvp.atmo.permissionSet` (permission sets can only reference NSIDs in their
77+// own namespace).
58export const collections = [
69 'community.lexicon.calendar.event',
710 'community.lexicon.calendar.rsvp'
···9121013export type AllowedCollection = (typeof collections)[number];
11141212-/** Permissioned-spaces XRPC methods the app needs to call on the user's behalf.
1313- * `aud: '*'` lets one consent cover dev (tunnel DID) and prod (published DID) without re-consenting. */
1414-const spaceMethods = [
1515- 'tools.atmo.space.admin.createSpace',
1616- 'tools.atmo.space.admin.addMember',
1717- 'tools.atmo.space.putRecord',
1818- 'tools.atmo.space.listRecords',
1919- 'tools.atmo.space.getRecord',
2020- 'tools.atmo.space.getSpace',
2121- 'tools.atmo.space.invite.create',
2222- 'tools.atmo.space.invite.redeem',
2323- 'tools.atmo.space.invite.list',
2424- 'tools.atmo.space.invite.revoke'
2525-] as const;
2626-2727-// OAuth scope — add scope.blob(), scope.rpc(), etc. as needed
1515+// OAuth scopes. `include:rsvp.atmo.permissionSet?aud=*` bundles every rpc method
1616+// the deployment exposes; `aud=*` lets the same consent cover dev (tunnel DID)
1717+// and prod (published DID) without re-consenting. Repo writes and blob uploads
1818+// live as standalone scopes since they reference NSIDs (or resource kinds)
1919+// outside the `rsvp.atmo` namespace.
2820export const scopes = [
2921 'atproto',
3022 scope.repo({ collection: [...collections] }),
3123 scope.blob({ accept: ['image/*'] }),
3232- ...spaceMethods.map((lxm) => scope.rpc({ lxm: [lxm], aud: '*' }))
2424+ 'include:rsvp.atmo.permissionSet'
3325];
34263527// set to false to disable signup
···22 * When the tunnel is running, this file is rewritten with the tunnel's
33 * service DID + URL; when the tunnel stops, it is reset to null values. */
4455-export const SERVICE_DID: string | null = "did:web:scroll-heat-flowers-software.trycloudflare.com#event_space";
66-export const SERVICE_URL: string | null = "https://scroll-heat-flowers-software.trycloudflare.com";
55+export const SERVICE_DID: string | null = "did:web:kid-retention-reservation-proc.trycloudflare.com";
66+export const SERVICE_URL: string | null = "https://kid-retention-reservation-proc.trycloudflare.com";
+10-2
src/routes/(app)/calendar/+page.server.ts
···44 getServerClient,
55 listEventRecordsFromContrail
66} from '$lib/contrail';
77+import { getSpacesClient } from '$lib/spaces/server/client';
88+import { spacesAvailable } from '$lib/spaces/config';
79import type { PageServerLoad } from './$types';
810911export const load: PageServerLoad = async ({ locals, platform }) => {
1010- const client = getServerClient(platform!.env.DB);
1112 if (!locals.did) {
1213 return { events: [], loggedIn: false };
1314 }
1515+ // Authenticated + spaces configured → use the service-auth client so the
1616+ // server unions public events with events from every space the user is in.
1717+ // Falls back to the unauthenticated client otherwise (public-only).
1818+ const client =
1919+ locals.client && spacesAvailable()
2020+ ? getSpacesClient(locals.client, platform!.env.DB)
2121+ : getServerClient(platform!.env.DB);
14221523 const now = new Date().toISOString();
16241725 const [rsvpResponse, hostingResponse] = await Promise.all([
1818- client.get('community.lexicon.calendar.rsvp.listRecords', {
2626+ client.get('rsvp.atmo.rsvp.listRecords', {
1927 params: { actor: locals.did, hydrateEvent: true, limit: 100 }
2028 }),
2129 listEventRecordsFromContrail(client, {
+24-2
src/routes/(app)/create/+page.svelte
···11<script lang="ts">
22 import EventEditor from '$lib/components/EventEditor.svelte';
33+ import { page } from '$app/state';
44+ import { goto } from '$app/navigation';
3546 let { data } = $props();
77+ let privateMode = $derived(page.url.searchParams.get('private') === '1');
88+99+ function toggle() {
1010+ const u = new URL(page.url);
1111+ if (privateMode) u.searchParams.delete('private');
1212+ else u.searchParams.set('private', '1');
1313+ goto(u.pathname + u.search, { replaceState: true });
1414+ }
515</script>
616717<svelte:head>
88- <title>Create Event</title>
1818+ <title>{privateMode ? 'Create Private Event' : 'Create Event'}</title>
919</svelte:head>
10201111-<EventEditor eventData={null} actorDid={data.actorDid} rkey={data.rkey} />
2121+<div class="mx-auto max-w-3xl px-4 pt-4">
2222+ <label class="flex cursor-pointer items-center gap-3 rounded-md border border-dashed border-base-300 bg-base-50 p-3 text-sm dark:border-base-700 dark:bg-base-900">
2323+ <input type="checkbox" checked={privateMode} onchange={toggle} class="size-4" />
2424+ <div>
2525+ <div class="font-medium">Private event</div>
2626+ <div class="text-xs text-base-500 dark:text-base-400">
2727+ Only people you add (or who redeem an invite link) can see the event. Not published to your public profile.
2828+ </div>
2929+ </div>
3030+ </label>
3131+</div>
3232+3333+<EventEditor eventData={null} actorDid={data.actorDid} rkey={data.rkey} {privateMode} />
+11-2
src/routes/(app)/p/[actor]/+page.server.ts
···66 listAttendingEventsFromContrail,
77 listEventRecordsFromContrail
88} from '$lib/contrail';
99+import { getSpacesClient } from '$lib/spaces/server/client';
1010+import { spacesAvailable } from '$lib/spaces/config';
911import { isActorIdentifier } from '@atcute/lexicons/syntax';
1012import { error } from '@sveltejs/kit';
11131214const PREVIEW_LIMIT = 6;
13151414-export async function load({ params, platform }) {
1515- const client = getServerClient(platform!.env.DB);
1616+export async function load({ params, platform, locals }) {
1717+ // Authenticated viewer + spaces configured → service-auth client so contrail
1818+ // unions public events with private events from spaces the viewer is in.
1919+ // Profile pages show another user's events; the viewer only sees the private
2020+ // ones where *they* are a member (filtered server-side by caller DID).
2121+ const client =
2222+ locals.client && locals.did && spacesAvailable()
2323+ ? getSpacesClient(locals.client, platform!.env.DB)
2424+ : getServerClient(platform!.env.DB);
1625 if (!isActorIdentifier(params.actor)) return;
17261827 const actor = params.actor;