+27
.tangled/workflows/deploy.yml
+27
.tangled/workflows/deploy.yml
···
1
+
# Set the following secrets in your repo's pipeline settings:
2
+
# RAILWAY_TOKEN
3
+
# RAILWAY_SERVICE_ID
4
+
5
+
when:
6
+
- event: ["push"]
7
+
branch: ["main"]
8
+
9
+
engine: "nixery"
10
+
11
+
dependencies:
12
+
nixpkgs:
13
+
- rustup
14
+
- gcc
15
+
16
+
steps:
17
+
- name: Install Rust toolchain
18
+
command: rustup default stable
19
+
20
+
- name: Install Railway CLI
21
+
command: cargo install railwayapp --locked
22
+
23
+
- name: Link `railway` executable
24
+
command: ln -s /tangled/home/.cargo/bin/railway /bin/railway
25
+
26
+
- name: Deploy to Railway
27
+
command: railway up --ci --service=$RAILWAY_SERVICE_ID
+1
-1
packages/appview/README.md
+1
-1
packages/appview/README.md
···
55
55
56
56
## API Endpoints
57
57
58
-
- `GET /client-metadata.json` - OAuth client metadata
58
+
- `GET /oauth-client-metadata.json` - OAuth client metadata
59
59
- `GET /oauth/callback` - OAuth callback endpoint
60
60
- `POST /login` - Login with handle
61
61
- `POST /logout` - Logout current user
+15
-2
packages/appview/src/api/oauth.ts
+15
-2
packages/appview/src/api/oauth.ts
···
9
9
const router = express.Router()
10
10
11
11
// OAuth metadata
12
-
router.get('/client-metadata.json', (_req, res) => {
12
+
router.get('/oauth-client-metadata.json', (_req, res) => {
13
13
res.json(ctx.oauthClient.clientMetadata)
14
14
})
15
15
···
51
51
router.post('/oauth/initiate', async (req, res) => {
52
52
// Validate
53
53
const handle = req.body?.handle
54
-
if (typeof handle !== 'string' || !isValidHandle(handle)) {
54
+
if (
55
+
typeof handle !== 'string' ||
56
+
!(isValidHandle(handle) || isValidUrl(handle))
57
+
) {
55
58
res.status(400).json({ error: 'Invalid handle' })
56
59
return
57
60
}
···
81
84
82
85
return router
83
86
}
87
+
88
+
function isValidUrl(url: string): boolean {
89
+
try {
90
+
const urlp = new URL(url)
91
+
// http or https
92
+
return urlp.protocol === 'http:' || urlp.protocol === 'https:'
93
+
} catch (error) {
94
+
return false
95
+
}
96
+
}
+1
-1
packages/appview/src/auth/client.ts
+1
-1
packages/appview/src/auth/client.ts
···
17
17
clientMetadata: {
18
18
client_name: 'Statusphere React App',
19
19
client_id: publicUrl
20
-
? `${url}/client-metadata.json`
20
+
? `${url}/oauth-client-metadata.json`
21
21
: `http://localhost?redirect_uri=${enc(`${url}/oauth/callback`)}&scope=${enc('atproto transition:generic')}`,
22
22
client_uri: url,
23
23
redirect_uris: [`${url}/oauth/callback`],
+1
-1
packages/appview/src/index.ts
+1
-1
packages/appview/src/index.ts
···
120
120
async close() {
121
121
this.ctx.logger.info('sigint received, shutting down')
122
122
await this.ctx.ingester.destroy()
123
-
return new Promise<void>((resolve) => {
123
+
await new Promise<void>((resolve) => {
124
124
this.server.close(() => {
125
125
this.ctx.logger.info('server closed')
126
126
resolve()
+7
-1
packages/appview/src/ingestors/jetstream.ts
+7
-1
packages/appview/src/ingestors/jetstream.ts
···
93
93
private cursor?: number
94
94
private ws?: WebSocket
95
95
private isStarted = false
96
+
private isDestroyed = false
96
97
private wantedCollections: string[]
97
98
98
99
constructor({
···
133
134
start() {
134
135
if (this.isStarted) return
135
136
this.isStarted = true
137
+
this.isDestroyed = false
136
138
this.ws = new WebSocket(this.constructUrlWithQuery())
137
139
138
140
this.ws.on('open', () => {
···
159
161
})
160
162
161
163
this.ws.on('close', (code, reason) => {
162
-
this.logger.error(`Jetstream closed. Code: ${code}, Reason: ${reason}`)
164
+
if (!this.isDestroyed) {
165
+
this.logger.error(`Jetstream closed. Code: ${code}, Reason: ${reason}`)
166
+
}
163
167
this.isStarted = false
164
168
})
165
169
}
166
170
167
171
destroy() {
168
172
if (this.ws) {
173
+
this.isDestroyed = true
169
174
this.ws.close()
170
175
this.isStarted = false
176
+
this.logger.info('jetstream destroyed gracefully')
171
177
}
172
178
}
173
179
}
+1
-1
packages/appview/src/lib/env.ts
+1
-1
packages/appview/src/lib/env.ts
···
15
15
COOKIE_SECRET: str({ devDefault: '0'.repeat(32) }),
16
16
SERVICE_DID: str({ default: undefined }),
17
17
PUBLIC_URL: str({ devDefault: '' }),
18
-
JETSTREAM_INSTANCE: str({ default: 'wss://jetstream.mozzius.dev' }),
18
+
JETSTREAM_INSTANCE: str({ default: 'wss://jetstream2.us-east.bsky.network' }),
19
19
})
+4
-1
packages/appview/src/lib/hydrate.ts
+4
-1
packages/appview/src/lib/hydrate.ts
···
7
7
import { AppContext } from '#/context'
8
8
import { Status } from '#/db'
9
9
10
+
const INVALID_HANDLE = 'handle.invalid'
11
+
10
12
export async function statusToStatusView(
11
13
status: Status,
12
14
ctx: AppContext,
···
19
21
did: status.authorDid,
20
22
handle: await ctx.resolver
21
23
.resolveDidToHandle(status.authorDid)
22
-
.catch(() => 'invalid.handle'),
24
+
.then((handle) => (handle.startsWith('did:') ? INVALID_HANDLE : handle))
25
+
.catch(() => INVALID_HANDLE),
23
26
},
24
27
}
25
28
}
+7
-2
packages/client/index.html
+7
-2
packages/client/index.html
···
1
-
<!DOCTYPE html>
1
+
<!doctype html>
2
2
<html lang="en">
3
3
<head>
4
4
<meta charset="UTF-8" />
5
5
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
6
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
<title>Statusphere React</title>
8
+
<script
9
+
defer
10
+
data-domain="statusphere.mozzius.dev"
11
+
src="https://plausible.mozzius.dev/js/script.js"
12
+
></script>
8
13
</head>
9
14
<body>
10
15
<div id="root"></div>
11
16
<script type="module" src="/src/main.tsx"></script>
12
17
</body>
13
-
</html>
18
+
</html>
+1
-1
packages/client/src/components/Header.tsx
+1
-1
packages/client/src/components/Header.tsx
···
31
31
<img
32
32
src={user.profile.avatar}
33
33
alt={user.profile.displayName || user.profile.handle}
34
-
className="w-8 h-8 rounded-full"
34
+
className="w-8 h-8 rounded-full text-transparent"
35
35
/>
36
36
) : (
37
37
<div className="w-8 h-8 bg-gray-200 dark:bg-gray-700 rounded-full"></div>
+5
-1
packages/client/src/pages/LoginPage.tsx
+5
-1
packages/client/src/pages/LoginPage.tsx
···
49
49
<Header />
50
50
51
51
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 shadow-sm max-w-md mx-auto w-full">
52
-
<h2 className="text-xl font-semibold mb-4">Login with your handle</h2>
52
+
<h2 className="text-xl font-semibold mb-4">Login with ATProto</h2>
53
53
54
54
{error && (
55
55
<div className="text-red-500 mb-4 p-2 bg-red-50 dark:bg-red-950 dark:bg-opacity-30 rounded-md">
···
74
74
disabled={pending}
75
75
className="w-full p-3 border border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-300 dark:focus:ring-blue-500 transition-colors"
76
76
/>
77
+
<p className="text-gray-400 dark:text-gray-500 text-sm mt-2">
78
+
You can also enter an AT Protocol PDS URL, i.e.{' '}
79
+
<span className="whitespace-nowrap">https://bsky.social</span>
80
+
</p>
77
81
</div>
78
82
79
83
<button