···210210}
211211212212/**
213213+ * Creates a new record (PDS rejects if the rkey already exists). Use this
214214+ * instead of putRecord when the OAuth grant only includes the `create` action
215215+ * for the target collection (e.g. `app.bsky.feed.post`).
216216+ */
217217+export async function createRecord({
218218+ collection,
219219+ rkey,
220220+ record
221221+}: {
222222+ collection: AllowedCollection;
223223+ rkey?: string;
224224+ record: Record<string, unknown>;
225225+}) {
226226+ if (!user.did) throw new Error('Not logged in');
227227+228228+ const { createRecord: createRecordRemote } = await import('./server/repo.remote');
229229+ const data = await createRecordRemote({ collection, rkey, record });
230230+ return { ok: true, data };
231231+}
232232+233233+/**
213234 * Deletes a record via remote function.
214235 */
215236export async function deleteRecord({
+28-2
src/lib/atproto/server/repo.remote.ts
···11import { error } from '@sveltejs/kit';
22import { command, getRequestEvent } from '$app/server';
33import * as v from 'valibot';
44-import { collections } from '../settings';
44+import { allowedCollections } from '../settings';
5566// Validate collection format and check against allowed list from settings
77const collectionSchema = v.pipe(
88 v.string(),
99 v.regex(/^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z][a-zA-Z0-9-]*){2,}$/),
1010- v.check((c) => collections.includes(c as (typeof collections)[number]), 'Collection not in allowed list')
1010+ v.check(
1111+ (c) => allowedCollections.includes(c as (typeof allowedCollections)[number]),
1212+ 'Collection not in allowed list'
1313+ )
1114);
12151316// AT Protocol rkey: TID, 'self', or other valid record keys (alphanumeric, dash, underscore, dot)
···2831 collection: input.collection as `${string}.${string}.${string}`,
2932 repo: locals.did,
3033 rkey: input.rkey || 'self',
3434+ record: input.record
3535+ }
3636+ });
3737+3838+ return response.data;
3939+ }
4040+);
4141+4242+export const createRecord = command(
4343+ v.object({
4444+ collection: collectionSchema,
4545+ rkey: rkeySchema,
4646+ record: v.record(v.string(), v.unknown())
4747+ }),
4848+ async (input) => {
4949+ const { locals } = getRequestEvent();
5050+ if (!locals.client || !locals.did) error(401, 'Not authenticated');
5151+5252+ const response = await locals.client.post('com.atproto.repo.createRecord', {
5353+ input: {
5454+ collection: input.collection as `${string}.${string}.${string}`,
5555+ repo: locals.did,
5656+ ...(input.rkey ? { rkey: input.rkey } : {}),
3157 record: input.record
3258 }
3359 });
+5-2
src/lib/atproto/settings.ts
···1010 'community.lexicon.calendar.rsvp'
1111] as const;
12121313-export type AllowedCollection = (typeof collections)[number];
1313+export const allowedCollections = [...collections, 'app.bsky.feed.post'];
1414+1515+export type AllowedCollection = (typeof allowedCollections)[number];
14161517// OAuth scopes. `include:rsvp.atmo.permissionSet?aud=*` bundles every rpc method
1618// the deployment exposes; `aud=*` lets the same consent cover dev (tunnel DID)
···2123 'atproto',
2224 scope.repo({ collection: [...collections] }),
2325 scope.blob({ accept: ['image/*'] }),
2424- 'include:rsvp.atmo.permissionSet'
2626+ 'include:rsvp.atmo.permissionSet',
2727+ 'include:app.bsky.authCreatePosts'
2528];
26292730// set to false to disable signup
···3232 accentColor: string;
3333 baseColor: string;
3434 };
3535+ /**
3636+ * Reference to a Bluesky post about this event. When present, the host has
3737+ * shared the event to their Bluesky feed; when `showComments` is true, the
3838+ * event page renders the post's reply thread as a comments section.
3939+ */
4040+ bskyPostRef?: {
4141+ uri: string;
4242+ cid: string;
4343+ showComments: boolean;
4444+ };
3545};