atmo.rsvp
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

spaces pt1

Florian b40d179a fac3f486

+138 -36
+3
.gitignore
··· 25 25 # Vite 26 26 vite.config.js.timestamp-* 27 27 vite.config.ts.timestamp-* 28 + 29 + # Tunnel-generated (only meaningful while tunnel is running) 30 + static/.well-known/did.json
+1 -1
package.json
··· 66 66 "dependencies": { 67 67 "@atcute/bluesky-richtext-parser": "^2.1.1", 68 68 "@atcute/jetstream": "^1.1.2", 69 - "@atmo-dev/contrail": "^0.0.6", 69 + "@atmo-dev/contrail": "link:../contrail", 70 70 "@ethercorps/sveltekit-og": "^4.2.1", 71 71 "@foxui/colors": "^0.8.4", 72 72 "@foxui/core": "^0.9.0",
+2 -28
pnpm-lock.yaml
··· 15 15 specifier: ^1.1.2 16 16 version: 1.1.2 17 17 '@atmo-dev/contrail': 18 - specifier: ^0.0.6 19 - version: 0.0.6(@atcute/identity@1.1.4) 18 + specifier: link:../contrail 19 + version: link:../contrail 20 20 '@ethercorps/sveltekit-og': 21 21 specifier: ^4.2.1 22 22 version: 4.2.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.0)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0)))(svelte@5.55.0)(typescript@6.0.2)(vite@8.0.3(@types/node@25.0.10)(esbuild@0.27.4)(jiti@2.6.1)(tsx@4.21.0))) ··· 266 266 267 267 '@atcute/varint@2.0.0': 268 268 resolution: {integrity: sha512-CEY/oVK/nVpL4e5y3sdenLETDL6/Xu5xsE/0TupK+f0Yv8jcD60t2gD8SHROWSvUwYLdkjczLCSA7YrtnjCzWw==} 269 - 270 - '@atmo-dev/contrail@0.0.6': 271 - resolution: {integrity: sha512-0cJ6Ftg2DJKp6MX1RCGHR7tHT8k7ZwWwJqjvdIfKN+ciVqsv5i0BvEBxs+MQ/0xF4wfddGYFxrAJRetLkzWU9w==} 272 - peerDependencies: 273 - pg: ^8.0.0 274 - peerDependenciesMeta: 275 - pg: 276 - optional: true 277 269 278 270 '@badrap/valita@0.4.6': 279 271 resolution: {integrity: sha512-4kdqcjyxo/8RQ8ayjms47HCWZIF5981oE5nIenbfThKDxWXtEHKipAOWlflpPJzZx9y/JWYQkp18Awr7VuepFg==} ··· 1991 1983 hls.js@1.6.15: 1992 1984 resolution: {integrity: sha512-E3a5VwgXimGHwpRGV+WxRTKeSp2DW5DI5MWv34ulL3t5UNmyJWCQ1KmLEHbYzcfThfXG8amBL+fCYPneGHC4VA==} 1993 1985 1994 - hono@4.12.9: 1995 - resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} 1996 - engines: {node: '>=16.9.0'} 1997 - 1998 1986 htmlparser2@10.1.0: 1999 1987 resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} 2000 1988 ··· 3111 3099 unicode-segmenter: 0.14.5 3112 3100 3113 3101 '@atcute/varint@2.0.0': {} 3114 - 3115 - '@atmo-dev/contrail@0.0.6(@atcute/identity@1.1.4)': 3116 - dependencies: 3117 - '@atcute/atproto': 3.1.10 3118 - '@atcute/client': 4.2.1 3119 - '@atcute/identity-resolver': 1.2.2(@atcute/identity@1.1.4) 3120 - '@atcute/jetstream': 1.1.2 3121 - '@atcute/lexicons': 1.2.9 3122 - hono: 4.12.9 3123 - transitivePeerDependencies: 3124 - - '@atcute/identity' 3125 - - react 3126 3102 3127 3103 '@badrap/valita@0.4.6': {} 3128 3104 ··· 4736 4712 highlight.js@11.11.1: {} 4737 4713 4738 4714 hls.js@1.6.15: {} 4739 - 4740 - hono@4.12.9: {} 4741 4715 4742 4716 htmlparser2@10.1.0: 4743 4717 dependencies:
+55 -2
src/lib/atproto/scripts/tunnel.ts
··· 1 - import { readFileSync, writeFileSync } from 'node:fs'; 2 - import { resolve } from 'node:path'; 1 + import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'; 2 + import { resolve, dirname } from 'node:path'; 3 3 import { spawn } from 'node:child_process'; 4 4 import { DEV_PORT } from '../port'; 5 5 6 6 const cwd = process.cwd(); 7 7 const envPath = resolve(cwd, '.env'); 8 8 const vitePath = resolve(cwd, 'vite.config.ts'); 9 + const didDocPath = resolve(cwd, 'static/.well-known/did.json'); 10 + const generatedServicePath = resolve(cwd, 'src/lib/spaces/tunnel-service.generated.ts'); 11 + 12 + /** Service fragment identifier used by our DID doc and service DID. */ 13 + const SERVICE_FRAGMENT = 'event_space'; 14 + const SERVICE_TYPE = 'AtmoSpaceService'; 9 15 10 16 let tunnelUrl: string | null = null; 11 17 let statusBarActive = false; ··· 133 139 writeFileSync(vitePath, vite); 134 140 } 135 141 142 + // ── did doc + generated service file ───────────────────────────── 143 + 144 + function writeDidDoc(hostname: string, tunnelUrl: string): void { 145 + const did = `did:web:${hostname}`; 146 + const doc = { 147 + '@context': ['https://www.w3.org/ns/did/v1'], 148 + id: did, 149 + service: [ 150 + { 151 + id: `#${SERVICE_FRAGMENT}`, 152 + type: SERVICE_TYPE, 153 + serviceEndpoint: tunnelUrl 154 + } 155 + ] 156 + }; 157 + mkdirSync(dirname(didDocPath), { recursive: true }); 158 + writeFileSync(didDocPath, JSON.stringify(doc, null, 2) + '\n'); 159 + } 160 + 161 + function writeGeneratedService(hostname: string, tunnelUrl: string): void { 162 + const did = `did:web:${hostname}#${SERVICE_FRAGMENT}`; 163 + const body = 164 + `/** Auto-generated by \`pnpm tunnel\`. Do not edit by hand.\n` + 165 + ` * When the tunnel is running, this file is rewritten with the tunnel's\n` + 166 + ` * service DID + URL; when the tunnel stops, it is reset to null values. */\n\n` + 167 + `export const SERVICE_DID: string | null = ${JSON.stringify(did)};\n` + 168 + `export const SERVICE_URL: string | null = ${JSON.stringify(tunnelUrl)};\n`; 169 + mkdirSync(dirname(generatedServicePath), { recursive: true }); 170 + writeFileSync(generatedServicePath, body); 171 + } 172 + 173 + function resetGeneratedService(): void { 174 + const body = 175 + `/** Auto-generated by \`pnpm tunnel\`. Do not edit by hand.\n` + 176 + ` * When the tunnel is running, this file is rewritten with the tunnel's\n` + 177 + ` * service DID + URL; when the tunnel stops, it is reset to null values. */\n\n` + 178 + `export const SERVICE_DID: string | null = null;\n` + 179 + `export const SERVICE_URL: string | null = null;\n`; 180 + writeFileSync(generatedServicePath, body); 181 + } 182 + 136 183 // ── cleanup ────────────────────────────────────────────────────── 137 184 138 185 function cleanup(): void { ··· 143 190 console.log(' Cleared OAUTH_PUBLIC_URL from .env'); 144 191 clearViteAllowedHosts(); 145 192 console.log(' Cleared allowedHosts from vite.config.ts'); 193 + resetGeneratedService(); 194 + console.log(' Reset src/lib/spaces/tunnel-service.generated.ts'); 146 195 } 147 196 } 148 197 ··· 163 212 164 213 setEnvVar('OAUTH_PUBLIC_URL', tunnelUrl); 165 214 setViteAllowedHosts(hostname); 215 + writeDidDoc(hostname, tunnelUrl); 216 + writeGeneratedService(hostname, tunnelUrl); 166 217 167 218 writeLog(`\n Set OAUTH_PUBLIC_URL=${tunnelUrl}\n`); 168 219 writeLog(` Set vite allowedHosts to [${hostname}]\n`); 220 + writeLog(` Wrote static/.well-known/did.json (did:web:${hostname}#${SERVICE_FRAGMENT})\n`); 221 + writeLog(` Wrote src/lib/spaces/tunnel-service.generated.ts\n`); 169 222 writeLog(` Tunnel is ready! Restart your dev server to pick up the new URL.\n\n`); 170 223 171 224 setupScrollRegion();
+17 -1
src/lib/atproto/settings.ts
··· 9 9 10 10 export type AllowedCollection = (typeof collections)[number]; 11 11 12 + /** Permissioned-spaces XRPC methods the app needs to call on the user's behalf. 13 + * `aud: '*'` lets one consent cover dev (tunnel DID) and prod (published DID) without re-consenting. */ 14 + const spaceMethods = [ 15 + 'tools.atmo.space.admin.createSpace', 16 + 'tools.atmo.space.admin.addMember', 17 + 'tools.atmo.space.putRecord', 18 + 'tools.atmo.space.listRecords', 19 + 'tools.atmo.space.getRecord', 20 + 'tools.atmo.space.getSpace', 21 + 'tools.atmo.space.invite.create', 22 + 'tools.atmo.space.invite.redeem', 23 + 'tools.atmo.space.invite.list', 24 + 'tools.atmo.space.invite.revoke' 25 + ] as const; 26 + 12 27 // OAuth scope — add scope.blob(), scope.rpc(), etc. as needed 13 28 export const scopes = [ 14 29 'atproto', 15 30 scope.repo({ collection: [...collections] }), 16 - scope.blob({ accept: ['image/*'] }) 31 + scope.blob({ accept: ['image/*'] }), 32 + ...spaceMethods.map((lxm) => scope.rpc({ lxm: [lxm], aud: '*' })) 17 33 ]; 18 34 19 35 // set to false to disable signup
+9 -1
src/lib/contrail/index.ts
··· 2 2 import { createHandler } from '@atmo-dev/contrail/server'; 3 3 import { Client } from '@atcute/client'; 4 4 import { config } from './config'; 5 + import { getSpacesConfig, spacesAvailable } from '../spaces/config'; 5 6 6 - export const contrail = new Contrail(config); 7 + const spaces = getSpacesConfig(); 8 + if (!spacesAvailable()) { 9 + console.warn( 10 + '[contrail/spaces] No service DID configured — spaces features will be inactive. Run `pnpm tunnel` in dev to enable.' 11 + ); 12 + } 13 + 14 + export const contrail = new Contrail({ ...config, ...(spaces ? { spaces } : {}) }); 7 15 8 16 let initialized = false; 9 17
+43
src/lib/spaces/config.ts
··· 1 + import type { SpacesConfig } from '@atmo-dev/contrail'; 2 + import { 3 + CompositeDidDocumentResolver, 4 + PlcDidDocumentResolver, 5 + WebDidDocumentResolver 6 + } from '@atcute/identity-resolver'; 7 + import { SERVICE_DID, SERVICE_URL } from './tunnel-service.generated'; 8 + 9 + /** The NSID identifying our kind of permissioned space. */ 10 + export const SPACE_TYPE = 'tools.atmo.event.space'; 11 + 12 + /** Per-collection policy for event-space records. */ 13 + const DEFAULT_POLICIES = { 14 + 'community.lexicon.calendar.event': { read: 'member' as const, write: 'owner' as const }, 15 + 'community.lexicon.calendar.rsvp': { read: 'member' as const, write: 'member' as const }, 16 + 'app.event.message': { read: 'member' as const, write: 'member' as const } 17 + }; 18 + 19 + /** Build the spaces config for contrail, or null if we can't run spaces 20 + * (no service DID => dev without tunnel, prod before service is published). */ 21 + export function getSpacesConfig(): SpacesConfig | null { 22 + if (!SERVICE_DID) { 23 + return null; 24 + } 25 + return { 26 + type: SPACE_TYPE, 27 + serviceDid: SERVICE_DID, 28 + resolver: new CompositeDidDocumentResolver({ 29 + methods: { 30 + plc: new PlcDidDocumentResolver(), 31 + web: new WebDidDocumentResolver() 32 + } 33 + }), 34 + defaultPolicies: DEFAULT_POLICIES 35 + }; 36 + } 37 + 38 + /** True if spaces are available in this environment. */ 39 + export function spacesAvailable(): boolean { 40 + return SERVICE_DID != null; 41 + } 42 + 43 + export { SERVICE_DID, SERVICE_URL };
+6
src/lib/spaces/tunnel-service.generated.ts
··· 1 + /** Auto-generated by `pnpm tunnel`. Do not edit by hand. 2 + * When the tunnel is running, this file is rewritten with the tunnel's 3 + * service DID + URL; when the tunnel stops, it is reset to null values. */ 4 + 5 + export const SERVICE_DID: string | null = "did:web:scroll-heat-flowers-software.trycloudflare.com#event_space"; 6 + export const SERVICE_URL: string | null = "https://scroll-heat-flowers-software.trycloudflare.com";
+1 -1
vite.config.ts
··· 9 9 server: { 10 10 host: '127.0.0.1', 11 11 port: DEV_PORT, 12 - allowedHosts: [] 12 + allowedHosts: ['scroll-heat-flowers-software.trycloudflare.com'] 13 13 } 14 14 });
+1 -2
wrangler.jsonc
··· 18 18 { 19 19 "binding": "DB", 20 20 "database_name": "atmo-events", 21 - "database_id": "b57d0c5a-e5ed-47f2-9f30-b39fa70cc068", 22 - "remote": true 21 + "database_id": "b57d0c5a-e5ed-47f2-9f30-b39fa70cc068" 23 22 } 24 23 ], 25 24 "triggers": {