@alteran/astro 0.5.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/db/client.ts +1 -1
- package/src/db/repo.ts +3 -3
- package/src/handlers/debug.ts +1 -1
- package/src/handlers/ready.ts +1 -1
- package/src/handlers/root.ts +1 -1
- package/src/handlers/xrpc.server.refreshSession.ts +6 -6
- package/src/lib/actor.ts +1 -1
- package/src/lib/blockstore-gc.ts +4 -4
- package/src/lib/chat.ts +6 -6
- package/src/lib/commit-log-pruning.ts +2 -2
- package/src/lib/feed.ts +4 -4
- package/src/lib/mst/blockstore.ts +7 -7
- package/src/lib/preferences.ts +3 -3
- package/src/lib/ratelimit.ts +4 -4
- package/src/lib/sequencer.ts +3 -3
- package/src/pages/debug/blob/[...key].ts +2 -2
- package/src/pages/debug/db/bootstrap.ts +1 -1
- package/src/pages/debug/db/commits.ts +1 -1
- package/src/pages/debug/gc/blobs.ts +1 -1
- package/src/pages/debug/sequencer.ts +3 -3
- package/src/pages/health.ts +4 -4
- package/src/pages/ready.ts +2 -2
- package/src/pages/xrpc/com.atproto.server.createSession.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.getBlob.ts +1 -1
- package/src/pages/xrpc/com.atproto.sync.listBlobs.ts +1 -1
- package/src/services/car.ts +4 -4
- package/src/services/r2-blob-store.ts +3 -3
- package/src/services/repo-manager.ts +6 -6
- package/src/worker/runtime.ts +30 -8
- package/src/worker/sequencer.ts +1 -1
- package/types/env.d.ts +3 -3
package/README.md
CHANGED
|
@@ -254,6 +254,18 @@ validateConfigOrThrow(env);
|
|
|
254
254
|
- Handle format is valid
|
|
255
255
|
- Numeric values are positive
|
|
256
256
|
|
|
257
|
+
### Cloudflare Security Rules
|
|
258
|
+
|
|
259
|
+
`com.atproto.server.refreshSession` is a valid bodyless `POST`. Production deployments must allow this request shape through to the XRPC handler:
|
|
260
|
+
|
|
261
|
+
```txt
|
|
262
|
+
(http.request.method eq "POST" and http.request.uri.path eq "/xrpc/com.atproto.server.refreshSession")
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Astro's SSR origin-check middleware rejects unsafe requests with no `Origin` header before project middleware runs. Alteran normalizes `/xrpc/*` requests at the Worker entrypoint so bearer-token XRPC clients can send bodyless POSTs without tripping Astro's form CSRF guard.
|
|
266
|
+
|
|
267
|
+
If Cloudflare WAF/API Shield also protects the deployment, keep any exception narrow to the expression above. This exception is not configured in `wrangler.jsonc`; Wrangler only manages the Worker deployment and bindings.
|
|
268
|
+
|
|
257
269
|
### Environment-Specific Settings
|
|
258
270
|
|
|
259
271
|
See [`wrangler.jsonc`](wrangler.jsonc:40) for environment-specific configurations:
|
package/package.json
CHANGED
package/src/db/client.ts
CHANGED
package/src/db/repo.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { encodeBlocksForCommit } from '../services/car';
|
|
|
10
10
|
import { ServerMisconfigured } from '../lib/errors';
|
|
11
11
|
|
|
12
12
|
export async function getRoot(env: Env) {
|
|
13
|
-
const db = drizzle(env.
|
|
13
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
14
14
|
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
15
15
|
return db.select().from(repo_root).where(eq(repo_root.did, did)).get();
|
|
16
16
|
}
|
|
@@ -30,7 +30,7 @@ export async function bumpRoot(env: Env, prevMstRoot?: CID, currentMstRoot?: CID
|
|
|
30
30
|
sig: string;
|
|
31
31
|
blocks: string; // base64-encoded CAR
|
|
32
32
|
}> {
|
|
33
|
-
const db = drizzle(env.
|
|
33
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
34
34
|
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
35
35
|
|
|
36
36
|
// Falls back to an ephemeral key only in non-production so dev runs work
|
|
@@ -107,7 +107,7 @@ export async function bumpRoot(env: Env, prevMstRoot?: CID, currentMstRoot?: CID
|
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
export async function appendCommit(env: Env, cid: string, rev: string, data: string, sig: string) {
|
|
110
|
-
const db = drizzle(env.
|
|
110
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
111
111
|
const ts = Date.now();
|
|
112
112
|
|
|
113
113
|
await db
|
package/src/handlers/debug.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { putRecord as dalPutRecord, getRecord as dalGetRecord } from '../db/dal'
|
|
|
3
3
|
|
|
4
4
|
export async function POST_db_bootstrap(ctx: APIContext) {
|
|
5
5
|
const env: any = (ctx.locals as any).runtime?.env ?? (ctx.locals as any) ?? (globalThis as any);
|
|
6
|
-
const db = env.
|
|
6
|
+
const db = env.ALTERAN_DB;
|
|
7
7
|
await db.exec("CREATE TABLE IF NOT EXISTS record (uri TEXT PRIMARY KEY, cid TEXT NOT NULL, json TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s','now')));");
|
|
8
8
|
await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT PRIMARY KEY, key TEXT NOT NULL, mime TEXT NOT NULL, size INTEGER NOT NULL);");
|
|
9
9
|
await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (record_uri TEXT NOT NULL, key TEXT NOT NULL);");
|
package/src/handlers/ready.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { APIContext } from 'astro';
|
|
|
2
2
|
|
|
3
3
|
export async function GET(ctx: APIContext) {
|
|
4
4
|
try {
|
|
5
|
-
const db = (ctx.locals as any).runtime?.env?.
|
|
5
|
+
const db = (ctx.locals as any).runtime?.env?.ALTERAN_DB ?? (ctx.locals as any).ALTERAN_DB ?? (globalThis as any).ALTERAN_DB;
|
|
6
6
|
if (db) {
|
|
7
7
|
await db.prepare('select 1').first();
|
|
8
8
|
}
|
package/src/handlers/root.ts
CHANGED
|
@@ -81,7 +81,7 @@ const HTML_TEMPLATE = (
|
|
|
81
81
|
<strong>DID:</strong>
|
|
82
82
|
<span class="pill">${did}</span>
|
|
83
83
|
</p>
|
|
84
|
-
<a href="https://github.com/alteran-
|
|
84
|
+
<a href="https://github.com/alteran-social/alteran" target="_blank" rel="noopener noreferrer">
|
|
85
85
|
<svg viewBox="0 0 24 24" role="img" aria-hidden="true" focusable="false">
|
|
86
86
|
<path
|
|
87
87
|
fill="currentColor"
|
|
@@ -23,9 +23,9 @@ export async function POST(ctx: APIContext) {
|
|
|
23
23
|
if (!ver || ver.payload.t !== 'refresh') return Response.json({ error: 'InvalidToken' }, { status: 401 });
|
|
24
24
|
|
|
25
25
|
const jtiOld = String(ver.payload.jti || '');
|
|
26
|
-
if (jtiOld && env.
|
|
27
|
-
await env.
|
|
28
|
-
const row: any = await env.
|
|
26
|
+
if (jtiOld && env.ALTERAN_DB) {
|
|
27
|
+
await env.ALTERAN_DB.exec('CREATE TABLE IF NOT EXISTS token_revocation (refresh_jti TEXT PRIMARY KEY, exp INTEGER NOT NULL);');
|
|
28
|
+
const row: any = await env.ALTERAN_DB.prepare('SELECT refresh_jti FROM token_revocation WHERE refresh_jti=?').bind(jtiOld).first();
|
|
29
29
|
if (row?.refresh_jti) return Response.json({ error: 'InvalidToken' }, { status: 401 });
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -34,9 +34,9 @@ export async function POST(ctx: APIContext) {
|
|
|
34
34
|
const jtiNew = crypto.randomUUID();
|
|
35
35
|
const accessJwt = await signJwt(ctx, { sub: did, handle, t: 'access' }, 'access');
|
|
36
36
|
const refreshJwt = await signJwt(ctx, { sub: did, handle, t: 'refresh', jti: jtiNew }, 'refresh');
|
|
37
|
-
if (jtiOld && ver.payload.exp && env.
|
|
38
|
-
await env.
|
|
39
|
-
await env.
|
|
37
|
+
if (jtiOld && ver.payload.exp && env.ALTERAN_DB) {
|
|
38
|
+
await env.ALTERAN_DB.exec('CREATE TABLE IF NOT EXISTS token_revocation (refresh_jti TEXT PRIMARY KEY, exp INTEGER NOT NULL);');
|
|
39
|
+
await env.ALTERAN_DB.prepare('INSERT OR REPLACE INTO token_revocation (refresh_jti, exp) VALUES (?,?)').bind(jtiOld, Number(ver.payload.exp)).run();
|
|
40
40
|
}
|
|
41
41
|
return Response.json({ did, handle, accessJwt, refreshJwt });
|
|
42
42
|
}
|
package/src/lib/actor.ts
CHANGED
|
@@ -53,7 +53,7 @@ export async function fetchProfileRecord(env: Env, did: string): Promise<Profile
|
|
|
53
53
|
const upperBound = `at://~`; // '~' sorts after all valid DIDs
|
|
54
54
|
|
|
55
55
|
// Find any profile record - scan from "at://" to "at://~" and filter in app
|
|
56
|
-
const fallback = await env.
|
|
56
|
+
const fallback = await env.ALTERAN_DB.prepare(
|
|
57
57
|
'SELECT json FROM record WHERE uri >= ? AND uri < ? ORDER BY rowid DESC LIMIT 50'
|
|
58
58
|
)
|
|
59
59
|
.bind(prefix, upperBound)
|
package/src/lib/blockstore-gc.ts
CHANGED
|
@@ -11,7 +11,7 @@ import * as dagCbor from '@ipld/dag-cbor';
|
|
|
11
11
|
* This traverses the MST structure to find all blocks that are still in use
|
|
12
12
|
*/
|
|
13
13
|
async function collectReferencedCids(env: Env, keepCommits: number = 10000): Promise<Set<string>> {
|
|
14
|
-
const db = drizzle(env.
|
|
14
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
15
15
|
const referenced = new Set<string>();
|
|
16
16
|
|
|
17
17
|
// Get recent commits
|
|
@@ -60,7 +60,7 @@ async function collectReferencedCids(env: Env, keepCommits: number = 10000): Pro
|
|
|
60
60
|
* Recursively traverse MST nodes to collect all CIDs
|
|
61
61
|
*/
|
|
62
62
|
async function traverseMst(env: Env, rootCid: string, referenced: Set<string>): Promise<void> {
|
|
63
|
-
const db = drizzle(env.
|
|
63
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
64
64
|
const visited = new Set<string>();
|
|
65
65
|
const queue = [rootCid];
|
|
66
66
|
|
|
@@ -128,7 +128,7 @@ async function traverseMst(env: Env, rootCid: string, referenced: Set<string>):
|
|
|
128
128
|
* @returns Number of blocks removed
|
|
129
129
|
*/
|
|
130
130
|
export async function pruneOrphanedBlocks(env: Env, keepCommits: number = 10000): Promise<number> {
|
|
131
|
-
const db = drizzle(env.
|
|
131
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
132
132
|
|
|
133
133
|
// Collect all CIDs referenced by recent commits
|
|
134
134
|
const referenced = await collectReferencedCids(env, keepCommits);
|
|
@@ -181,7 +181,7 @@ export async function getBlockstoreStats(env: Env): Promise<{
|
|
|
181
181
|
total: number;
|
|
182
182
|
totalSize: number;
|
|
183
183
|
}> {
|
|
184
|
-
const db = drizzle(env.
|
|
184
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
185
185
|
const blocks = await db.select().from(blockstore).all();
|
|
186
186
|
|
|
187
187
|
const totalSize = blocks.reduce((sum, block) => {
|
package/src/lib/chat.ts
CHANGED
|
@@ -33,7 +33,7 @@ export async function ensureChatTables(env: Env) {
|
|
|
33
33
|
if (tablesEnsured) return;
|
|
34
34
|
|
|
35
35
|
// Create chat_convo table
|
|
36
|
-
await env.
|
|
36
|
+
await env.ALTERAN_DB.prepare(
|
|
37
37
|
'CREATE TABLE IF NOT EXISTS chat_convo (' +
|
|
38
38
|
'id TEXT PRIMARY KEY, ' +
|
|
39
39
|
'rev TEXT NOT NULL, ' +
|
|
@@ -48,7 +48,7 @@ export async function ensureChatTables(env: Env) {
|
|
|
48
48
|
).run();
|
|
49
49
|
|
|
50
50
|
// Create chat_convo_member table
|
|
51
|
-
await env.
|
|
51
|
+
await env.ALTERAN_DB.prepare(
|
|
52
52
|
'CREATE TABLE IF NOT EXISTS chat_convo_member (' +
|
|
53
53
|
'convo_id TEXT NOT NULL, ' +
|
|
54
54
|
'did TEXT NOT NULL, ' +
|
|
@@ -61,7 +61,7 @@ export async function ensureChatTables(env: Env) {
|
|
|
61
61
|
).run();
|
|
62
62
|
|
|
63
63
|
// Create index
|
|
64
|
-
await env.
|
|
64
|
+
await env.ALTERAN_DB.prepare(
|
|
65
65
|
'CREATE INDEX IF NOT EXISTS chat_convo_member_did_idx ON chat_convo_member (did)'
|
|
66
66
|
).run();
|
|
67
67
|
|
|
@@ -103,7 +103,7 @@ export async function listChatConvos(
|
|
|
103
103
|
query += ' ORDER BY rowid DESC LIMIT ?';
|
|
104
104
|
params.push(limit);
|
|
105
105
|
|
|
106
|
-
const result = await env.
|
|
106
|
+
const result = await env.ALTERAN_DB.prepare(query).bind(...params).all<{
|
|
107
107
|
rowid: number;
|
|
108
108
|
id: string;
|
|
109
109
|
rev: string;
|
|
@@ -119,7 +119,7 @@ export async function listChatConvos(
|
|
|
119
119
|
|
|
120
120
|
if (result.results) {
|
|
121
121
|
for (const row of result.results) {
|
|
122
|
-
const members = await env.
|
|
122
|
+
const members = await env.ALTERAN_DB.prepare(
|
|
123
123
|
`SELECT did, handle, display_name, avatar FROM chat_convo_member WHERE convo_id = ? ORDER BY position ASC`
|
|
124
124
|
)
|
|
125
125
|
.bind(row.id)
|
|
@@ -186,7 +186,7 @@ export async function listChatConvoLogs(env: Env, did: string, cursor?: number,
|
|
|
186
186
|
query += ' ORDER BY rowid DESC LIMIT ?';
|
|
187
187
|
params.push(limit);
|
|
188
188
|
|
|
189
|
-
const result = await env.
|
|
189
|
+
const result = await env.ALTERAN_DB.prepare(query).bind(...params).all<{
|
|
190
190
|
rowid: number;
|
|
191
191
|
id: string;
|
|
192
192
|
rev: string;
|
|
@@ -18,7 +18,7 @@ import { logger } from './logger';
|
|
|
18
18
|
* @returns Number of commits pruned
|
|
19
19
|
*/
|
|
20
20
|
export async function pruneOldCommits(env: Env, keepCount: number = 10000): Promise<number> {
|
|
21
|
-
const db = drizzle(env.
|
|
21
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
22
22
|
|
|
23
23
|
// Get the sequence number of the Nth most recent commit
|
|
24
24
|
const threshold = await db
|
|
@@ -60,7 +60,7 @@ export async function getCommitLogStats(env: Env): Promise<{
|
|
|
60
60
|
oldest: number | null;
|
|
61
61
|
newest: number | null;
|
|
62
62
|
}> {
|
|
63
|
-
const db = drizzle(env.
|
|
63
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
64
64
|
|
|
65
65
|
const [oldest, newest, count] = await Promise.all([
|
|
66
66
|
db.select({ seq: commit_log.seq }).from(commit_log).orderBy(commit_log.seq).limit(1).get(),
|
package/src/lib/feed.ts
CHANGED
|
@@ -71,7 +71,7 @@ export async function listPosts(env: Env, limit: number, cursor?: string): Promi
|
|
|
71
71
|
}
|
|
72
72
|
params.push(safeLimit);
|
|
73
73
|
|
|
74
|
-
const response = await env.
|
|
74
|
+
const response = await env.ALTERAN_DB.prepare(
|
|
75
75
|
`SELECT rowid, uri, cid, json FROM record WHERE ${where} ORDER BY rowid DESC LIMIT ?`
|
|
76
76
|
)
|
|
77
77
|
.bind(...params)
|
|
@@ -84,7 +84,7 @@ export async function listPosts(env: Env, limit: number, cursor?: string): Promi
|
|
|
84
84
|
export async function getPostsByUris(env: Env, uris: string[]): Promise<ParsedPost[]> {
|
|
85
85
|
if (!uris.length) return [];
|
|
86
86
|
const placeholders = uris.map(() => '?').join(',');
|
|
87
|
-
const response = await env.
|
|
87
|
+
const response = await env.ALTERAN_DB.prepare(
|
|
88
88
|
`SELECT rowid, uri, cid, json FROM record WHERE uri IN (${placeholders})`
|
|
89
89
|
)
|
|
90
90
|
.bind(...uris)
|
|
@@ -142,7 +142,7 @@ export async function countPosts(env: Env): Promise<number> {
|
|
|
142
142
|
const actor = await getPrimaryActor(env);
|
|
143
143
|
const prefix = `at://${actor.did}/${POST_COLLECTION}/`;
|
|
144
144
|
const upperBound = `${prefix}{`; // '{' sorts after 'z', safely bounding rkeys
|
|
145
|
-
const response = await env.
|
|
145
|
+
const response = await env.ALTERAN_DB.prepare(
|
|
146
146
|
'SELECT COUNT(*) as count FROM record WHERE uri >= ? AND uri < ?'
|
|
147
147
|
)
|
|
148
148
|
.bind(prefix, upperBound)
|
|
@@ -151,7 +151,7 @@ export async function countPosts(env: Env): Promise<number> {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
export async function getPostByUri(env: Env, uri: string): Promise<ParsedPost | null> {
|
|
154
|
-
const response = await env.
|
|
154
|
+
const response = await env.ALTERAN_DB.prepare(
|
|
155
155
|
'SELECT rowid, uri, cid, json FROM record WHERE uri = ? LIMIT 1'
|
|
156
156
|
)
|
|
157
157
|
.bind(uri)
|
|
@@ -31,7 +31,7 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
31
31
|
constructor(private env: Env) {}
|
|
32
32
|
|
|
33
33
|
async get(cid: CID): Promise<Uint8Array | null> {
|
|
34
|
-
const row = await this.env.
|
|
34
|
+
const row = await this.env.ALTERAN_DB.prepare(
|
|
35
35
|
`SELECT bytes FROM blockstore WHERE cid = ? LIMIT 1`
|
|
36
36
|
).bind(cid.toString()).first();
|
|
37
37
|
|
|
@@ -43,7 +43,7 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
43
43
|
|
|
44
44
|
async has(cid: CID): Promise<boolean> {
|
|
45
45
|
// Treat rows with NULL or empty bytes as missing
|
|
46
|
-
const row = await this.env.
|
|
46
|
+
const row = await this.env.ALTERAN_DB.prepare(
|
|
47
47
|
`SELECT bytes FROM blockstore WHERE cid = ? LIMIT 1`
|
|
48
48
|
).bind(cid.toString()).first();
|
|
49
49
|
|
|
@@ -65,7 +65,7 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
65
65
|
for (let i = 0; i < cids.length; i += BATCH) {
|
|
66
66
|
const chunk = cids.slice(i, i + BATCH);
|
|
67
67
|
const placeholders = new Array(chunk.length).fill('?').join(',');
|
|
68
|
-
const stmt = this.env.
|
|
68
|
+
const stmt = this.env.ALTERAN_DB.prepare(`SELECT cid, bytes FROM blockstore WHERE cid IN (${placeholders})`);
|
|
69
69
|
const binds = chunk.map((c) => c.toString());
|
|
70
70
|
const response = await stmt.bind(...binds).all();
|
|
71
71
|
const rows = (response.results ?? []) as Array<{ cid: string; bytes: string }>;
|
|
@@ -97,7 +97,7 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
97
97
|
|
|
98
98
|
// Always upsert: replace rows with NULL/empty bytes
|
|
99
99
|
try {
|
|
100
|
-
await this.env.
|
|
100
|
+
await this.env.ALTERAN_DB.prepare(
|
|
101
101
|
`INSERT OR REPLACE INTO blockstore (cid, bytes) VALUES (?, ?)`
|
|
102
102
|
).bind(cidStr, base64).run();
|
|
103
103
|
} catch (error) {
|
|
@@ -117,7 +117,7 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
117
117
|
const entries = Array.from(blocks.entries());
|
|
118
118
|
for (let i = 0; i < entries.length; i += BATCH_SIZE) {
|
|
119
119
|
const batch = entries.slice(i, i + BATCH_SIZE);
|
|
120
|
-
const stmts = [] as Array<ReturnType<typeof this.env.
|
|
120
|
+
const stmts = [] as Array<ReturnType<typeof this.env.ALTERAN_DB['prepare']>>;
|
|
121
121
|
for (const [cid, bytes] of batch) {
|
|
122
122
|
const cidStr = cid.toString();
|
|
123
123
|
let binary = '';
|
|
@@ -127,12 +127,12 @@ export class D1Blockstore implements WritableBlockstore {
|
|
|
127
127
|
}
|
|
128
128
|
const base64 = btoa(binary);
|
|
129
129
|
stmts.push(
|
|
130
|
-
this.env.
|
|
130
|
+
this.env.ALTERAN_DB.prepare(`INSERT OR REPLACE INTO blockstore (cid, bytes) VALUES (?, ?)`)
|
|
131
131
|
.bind(cidStr, base64)
|
|
132
132
|
);
|
|
133
133
|
}
|
|
134
134
|
if (stmts.length > 0) {
|
|
135
|
-
await this.env.
|
|
135
|
+
await this.env.ALTERAN_DB.batch(stmts);
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
}
|
package/src/lib/preferences.ts
CHANGED
|
@@ -6,7 +6,7 @@ let tableEnsured = false;
|
|
|
6
6
|
|
|
7
7
|
async function ensureTable(env: Env) {
|
|
8
8
|
if (tableEnsured) return;
|
|
9
|
-
await env.
|
|
9
|
+
await env.ALTERAN_DB.exec(
|
|
10
10
|
'CREATE TABLE IF NOT EXISTS actor_preferences (did TEXT PRIMARY KEY, json TEXT NOT NULL, updated_at INTEGER NOT NULL)'
|
|
11
11
|
);
|
|
12
12
|
tableEnsured = true;
|
|
@@ -18,7 +18,7 @@ export async function getActorPreferences(env: Env): Promise<{ did: string; pref
|
|
|
18
18
|
await ensureTable(env);
|
|
19
19
|
const did = await resolveSecret(env.PDS_DID);
|
|
20
20
|
if (!did) throw new ServerMisconfigured('PDS_DID is not configured');
|
|
21
|
-
const row = await env.
|
|
21
|
+
const row = await env.ALTERAN_DB.prepare('SELECT json FROM actor_preferences WHERE did = ?')
|
|
22
22
|
.bind(did)
|
|
23
23
|
.first<{ json: string }>();
|
|
24
24
|
|
|
@@ -40,7 +40,7 @@ export async function setActorPreferences(env: Env, preferences: any[]): Promise
|
|
|
40
40
|
const did = await resolveSecret(env.PDS_DID);
|
|
41
41
|
if (!did) throw new ServerMisconfigured('PDS_DID is not configured');
|
|
42
42
|
const now = Date.now();
|
|
43
|
-
await env.
|
|
43
|
+
await env.ALTERAN_DB.prepare(
|
|
44
44
|
'INSERT INTO actor_preferences (did, json, updated_at) VALUES (?, ?, ?) ON CONFLICT(did) DO UPDATE SET json = excluded.json, updated_at = excluded.updated_at'
|
|
45
45
|
)
|
|
46
46
|
.bind(did, JSON.stringify(preferences ?? []), now)
|
package/src/lib/ratelimit.ts
CHANGED
|
@@ -8,14 +8,14 @@ export async function checkRate(env: Env, request: Request, bucket: 'writes' | '
|
|
|
8
8
|
const windowMs = 60_000;
|
|
9
9
|
const win = Math.floor(now / windowMs);
|
|
10
10
|
const ip = request.headers.get('cf-connecting-ip') ?? request.headers.get('x-forwarded-for') ?? '127.0.0.1';
|
|
11
|
-
await env.
|
|
12
|
-
const row: any = await env.
|
|
11
|
+
await env.ALTERAN_DB.exec("CREATE TABLE IF NOT EXISTS rate_limit (ip TEXT NOT NULL, bucket TEXT NOT NULL, window INTEGER NOT NULL, count INTEGER NOT NULL, PRIMARY KEY (ip,bucket,window))");
|
|
12
|
+
const row: any = await env.ALTERAN_DB.prepare('SELECT count FROM rate_limit WHERE ip=? AND bucket=? AND window=?').bind(ip, bucket, win).first();
|
|
13
13
|
const count = row?.count ? Number(row.count) : 0;
|
|
14
14
|
if (count >= limit) return rateLimited();
|
|
15
15
|
if (count === 0) {
|
|
16
|
-
await env.
|
|
16
|
+
await env.ALTERAN_DB.prepare('INSERT OR REPLACE INTO rate_limit (ip,bucket,window,count) VALUES (?,?,?,1)').bind(ip, bucket, win).run();
|
|
17
17
|
} else {
|
|
18
|
-
await env.
|
|
18
|
+
await env.ALTERAN_DB.prepare('UPDATE rate_limit SET count=count+1 WHERE ip=? AND bucket=? AND window=?').bind(ip, bucket, win).run();
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const headers = new Headers();
|
package/src/lib/sequencer.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { Env } from '../env';
|
|
2
2
|
|
|
3
3
|
export async function notifySequencer(env: Env, obj: unknown) {
|
|
4
|
-
if (!env.
|
|
4
|
+
if (!env.ALTERAN_SEQUENCER) {
|
|
5
5
|
console.warn('notifySequencer: SEQUENCER binding missing');
|
|
6
6
|
return;
|
|
7
7
|
}
|
|
8
8
|
try {
|
|
9
|
-
const id = env.
|
|
10
|
-
const stub = env.
|
|
9
|
+
const id = env.ALTERAN_SEQUENCER.idFromName('default');
|
|
10
|
+
const stub = env.ALTERAN_SEQUENCER.get(id);
|
|
11
11
|
await stub.fetch('https://sequencer/commit', {
|
|
12
12
|
method: 'POST',
|
|
13
13
|
headers: { 'content-type': 'application/json' },
|
|
@@ -7,7 +7,7 @@ export async function GET({ locals, params }: APIContext) {
|
|
|
7
7
|
const key = params.key;
|
|
8
8
|
if (!key) return new Response('missing key', { status: 400 });
|
|
9
9
|
|
|
10
|
-
const obj = await env.
|
|
10
|
+
const obj = await env.ALTERAN_BLOBS.get(key);
|
|
11
11
|
if (!obj) return new Response('not found', { status: 404 });
|
|
12
12
|
|
|
13
13
|
const body = obj.body as unknown as BodyInit | null;
|
|
@@ -22,6 +22,6 @@ export async function PUT({ locals, request, params }: APIContext) {
|
|
|
22
22
|
if (!key) return new Response('missing key', { status: 400 });
|
|
23
23
|
|
|
24
24
|
const body = await request.arrayBuffer();
|
|
25
|
-
await env.
|
|
25
|
+
await env.ALTERAN_BLOBS.put(key, body, { httpMetadata: { contentType: request.headers.get('content-type') ?? 'application/octet-stream' } });
|
|
26
26
|
return new Response('uploaded');
|
|
27
27
|
}
|
|
@@ -11,7 +11,7 @@ export async function POST({ locals }: APIContext) {
|
|
|
11
11
|
if (!isLocal) {
|
|
12
12
|
return new Response('Not Found', { status: 404 });
|
|
13
13
|
}
|
|
14
|
-
const db = env.
|
|
14
|
+
const db = env.ALTERAN_DB;
|
|
15
15
|
await db.exec("CREATE TABLE IF NOT EXISTS record (uri TEXT PRIMARY KEY, cid TEXT NOT NULL, json TEXT NOT NULL, created_at INTEGER DEFAULT (strftime('%s','now')));");
|
|
16
16
|
await db.exec("CREATE TABLE IF NOT EXISTS blob (cid TEXT PRIMARY KEY, key TEXT NOT NULL, mime TEXT NOT NULL, size INTEGER NOT NULL);");
|
|
17
17
|
await db.exec("CREATE TABLE IF NOT EXISTS blob_usage (record_uri TEXT NOT NULL, key TEXT NOT NULL);");
|
|
@@ -14,7 +14,7 @@ export async function GET({ locals, request }: APIContext) {
|
|
|
14
14
|
|
|
15
15
|
const url = new URL(request.url);
|
|
16
16
|
const n = Math.min(Number(url.searchParams.get('n') ?? '20') || 20, 200);
|
|
17
|
-
const db = drizzle(env.
|
|
17
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
18
18
|
const rows = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(n).all();
|
|
19
19
|
return new Response(JSON.stringify({ commits: rows }), { headers: { 'content-type': 'application/json' } });
|
|
20
20
|
}
|
|
@@ -8,7 +8,7 @@ export async function POST({ locals }: APIContext) {
|
|
|
8
8
|
const keys = await listOrphanBlobKeys(env);
|
|
9
9
|
let deleted = 0;
|
|
10
10
|
for (const key of keys) {
|
|
11
|
-
await env.
|
|
11
|
+
await env.ALTERAN_BLOBS.delete(key).catch(() => {});
|
|
12
12
|
await deleteBlobByKey(env, key);
|
|
13
13
|
deleted++;
|
|
14
14
|
}
|
|
@@ -6,7 +6,7 @@ export const prerender = false;
|
|
|
6
6
|
export async function GET({ locals }: APIContext) {
|
|
7
7
|
const { env } = locals.runtime;
|
|
8
8
|
|
|
9
|
-
if (!env.
|
|
9
|
+
if (!env.ALTERAN_SEQUENCER) {
|
|
10
10
|
return new Response(JSON.stringify({ error: 'SequencerNotConfigured' }), {
|
|
11
11
|
status: 503,
|
|
12
12
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -14,8 +14,8 @@ export async function GET({ locals }: APIContext) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
|
-
const id = env.
|
|
18
|
-
const stub = env.
|
|
17
|
+
const id = env.ALTERAN_SEQUENCER.idFromName('default');
|
|
18
|
+
const stub = env.ALTERAN_SEQUENCER.get(id);
|
|
19
19
|
const response = await stub.fetch(new Request('http://internal/metrics') as any);
|
|
20
20
|
const text = await response.text();
|
|
21
21
|
return new Response(text, { status: response.status, headers: { 'Content-Type': 'application/json' } });
|
package/src/pages/health.ts
CHANGED
|
@@ -22,8 +22,8 @@ export async function GET({ locals }: APIContext) {
|
|
|
22
22
|
|
|
23
23
|
// Check D1 database connectivity
|
|
24
24
|
try {
|
|
25
|
-
if (env.
|
|
26
|
-
await env.
|
|
25
|
+
if (env.ALTERAN_DB) {
|
|
26
|
+
await env.ALTERAN_DB.prepare('SELECT 1').first();
|
|
27
27
|
checks.database.status = 'ok';
|
|
28
28
|
} else {
|
|
29
29
|
checks.database.status = 'error';
|
|
@@ -38,9 +38,9 @@ export async function GET({ locals }: APIContext) {
|
|
|
38
38
|
|
|
39
39
|
// Check R2 storage connectivity
|
|
40
40
|
try {
|
|
41
|
-
if (env.
|
|
41
|
+
if (env.ALTERAN_BLOBS) {
|
|
42
42
|
// Simple list operation to verify connectivity
|
|
43
|
-
await env.
|
|
43
|
+
await env.ALTERAN_BLOBS.list({ limit: 1 });
|
|
44
44
|
checks.storage.status = 'ok';
|
|
45
45
|
} else {
|
|
46
46
|
checks.storage.status = 'error';
|
package/src/pages/ready.ts
CHANGED
|
@@ -6,8 +6,8 @@ export async function GET({ locals }: APIContext) {
|
|
|
6
6
|
const { env } = locals.runtime;
|
|
7
7
|
|
|
8
8
|
try {
|
|
9
|
-
if (env.
|
|
10
|
-
await env.
|
|
9
|
+
if (env.ALTERAN_DB) {
|
|
10
|
+
await env.ALTERAN_DB.prepare('select 1').first();
|
|
11
11
|
}
|
|
12
12
|
return new Response('ok');
|
|
13
13
|
} catch (e) {
|
|
@@ -18,7 +18,7 @@ export async function POST({ locals, request }: APIContext) {
|
|
|
18
18
|
const { env } = locals.runtime;
|
|
19
19
|
const clientIp = request.headers.get('cf-connecting-ip') || request.headers.get('x-forwarded-for') || 'unknown';
|
|
20
20
|
|
|
21
|
-
const db = drizzle(env.
|
|
21
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
22
22
|
const now = Math.floor(Date.now() / 1000);
|
|
23
23
|
|
|
24
24
|
// Check if IP is locked out
|
package/src/services/car.ts
CHANGED
|
@@ -24,7 +24,7 @@ export async function encodeRecordBlock(value: unknown) {
|
|
|
24
24
|
|
|
25
25
|
export async function buildRepoCar(env: Env, did: string): Promise<CarSnapshot> {
|
|
26
26
|
// Prefer the latest signed commit from commit_log (authoritative root)
|
|
27
|
-
const db = drizzle(env.
|
|
27
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
28
28
|
const tip = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(1).get();
|
|
29
29
|
|
|
30
30
|
if (tip) {
|
|
@@ -99,7 +99,7 @@ export async function buildRepoCar(env: Env, did: string): Promise<CarSnapshot>
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export async function buildRepoCarRange(env: Env, fromSeq: number, toSeq: number): Promise<CarSnapshot> {
|
|
102
|
-
const db = drizzle(env.
|
|
102
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
103
103
|
const rows = await db.select().from(commit_log).where(and(gte(commit_log.seq, fromSeq), lte(commit_log.seq, toSeq))).all();
|
|
104
104
|
const blocks: { cid: CID; bytes: Uint8Array }[] = [];
|
|
105
105
|
for (const r of rows) {
|
|
@@ -189,7 +189,7 @@ export async function encodeBlocksForCommit(
|
|
|
189
189
|
// Attempt to reconstruct commit block from commit_log if this is the commit cid
|
|
190
190
|
if (cidStr === commitCid.toString()) {
|
|
191
191
|
try {
|
|
192
|
-
const row = await (env.
|
|
192
|
+
const row = await (env.ALTERAN_DB as any)
|
|
193
193
|
.prepare('SELECT data, sig FROM commit_log WHERE cid = ? LIMIT 1')
|
|
194
194
|
.bind(cidStr)
|
|
195
195
|
.first();
|
|
@@ -260,7 +260,7 @@ export async function buildRecordProofCar(
|
|
|
260
260
|
collection: string,
|
|
261
261
|
rkey: string,
|
|
262
262
|
): Promise<{ bytes: Uint8Array }> {
|
|
263
|
-
const db = drizzle(env.
|
|
263
|
+
const db = drizzle(env.ALTERAN_DB);
|
|
264
264
|
const tip = await db.select().from(commit_log).orderBy(desc(commit_log.seq)).limit(1).get();
|
|
265
265
|
if (!tip) {
|
|
266
266
|
throw new NotFound('HeadNotFound');
|
|
@@ -73,15 +73,15 @@ export class R2BlobStore {
|
|
|
73
73
|
const shaB64 = R2BlobStore.b64url(sha);
|
|
74
74
|
const key = R2BlobStore.cidKey(shaB64);
|
|
75
75
|
const buffer = R2BlobStore.toArrayBuffer(view);
|
|
76
|
-
await this.env.
|
|
76
|
+
await this.env.ALTERAN_BLOBS.put(key, buffer, { httpMetadata: { contentType } });
|
|
77
77
|
return { key, size, sha256: shaB64 };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
async get(key: string): Promise<R2ObjectBody | null> {
|
|
81
|
-
return this.env.
|
|
81
|
+
return this.env.ALTERAN_BLOBS.get(key);
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
async delete(key: string): Promise<void> {
|
|
85
|
-
await this.env.
|
|
85
|
+
await this.env.ALTERAN_BLOBS.delete(key);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
@@ -46,7 +46,7 @@ export class RepoManager {
|
|
|
46
46
|
|
|
47
47
|
async getRoot(): Promise<MST | null> {
|
|
48
48
|
try {
|
|
49
|
-
const db = drizzle(this.env.
|
|
49
|
+
const db = drizzle(this.env.ALTERAN_DB);
|
|
50
50
|
const did = await this.getDid();
|
|
51
51
|
|
|
52
52
|
const rows = await db
|
|
@@ -58,7 +58,7 @@ export class RepoManager {
|
|
|
58
58
|
const row = rows[0];
|
|
59
59
|
if (!row) return null;
|
|
60
60
|
|
|
61
|
-
const commit = await this.env.
|
|
61
|
+
const commit = await this.env.ALTERAN_DB.prepare(
|
|
62
62
|
`SELECT data FROM commit_log WHERE cid = ? LIMIT 1`,
|
|
63
63
|
)
|
|
64
64
|
.bind(row.commitCid)
|
|
@@ -215,7 +215,7 @@ export class RepoManager {
|
|
|
215
215
|
const did = await this.getDid();
|
|
216
216
|
const uri = `at://${did}/${collection}/${rkey}`;
|
|
217
217
|
|
|
218
|
-
const result = await this.env.
|
|
218
|
+
const result = await this.env.ALTERAN_DB.prepare(`SELECT json FROM record WHERE uri = ?`)
|
|
219
219
|
.bind(uri)
|
|
220
220
|
.first();
|
|
221
221
|
|
|
@@ -265,10 +265,10 @@ export class RepoManager {
|
|
|
265
265
|
String.fromCharCode(prefix.charCodeAt(prefix.length - 1) + 1);
|
|
266
266
|
|
|
267
267
|
const stmt = cursor
|
|
268
|
-
? this.env.
|
|
268
|
+
? this.env.ALTERAN_DB.prepare(
|
|
269
269
|
`SELECT uri, cid FROM record WHERE uri >= ? AND uri < ? AND uri > ? ORDER BY uri LIMIT ?`,
|
|
270
270
|
).bind(prefix, rangeEnd, prefix + cursor, limit)
|
|
271
|
-
: this.env.
|
|
271
|
+
: this.env.ALTERAN_DB.prepare(
|
|
272
272
|
`SELECT uri, cid FROM record WHERE uri >= ? AND uri < ? ORDER BY uri LIMIT ?`,
|
|
273
273
|
).bind(prefix, rangeEnd, limit);
|
|
274
274
|
|
|
@@ -282,7 +282,7 @@ export class RepoManager {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
async updateRoot(mst: MST, rev: number): Promise<void> {
|
|
285
|
-
const db = drizzle(this.env.
|
|
285
|
+
const db = drizzle(this.env.ALTERAN_DB);
|
|
286
286
|
const rootCid = await mst.getPointer();
|
|
287
287
|
const did = await this.getDid();
|
|
288
288
|
const revStr = String(rev);
|
package/src/worker/runtime.ts
CHANGED
|
@@ -75,7 +75,7 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
|
|
|
75
75
|
return new Response(null, { status: 204, headers }) as unknown as WorkersResponse;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
await seed(resolvedEnv.
|
|
78
|
+
await seed(resolvedEnv.ALTERAN_DB, resolvedEnv.PDS_DID as string);
|
|
79
79
|
|
|
80
80
|
// Fire-and-forget: let relays know this PDS exists and is reachable.
|
|
81
81
|
// Throttled per isolate and safe to call frequently.
|
|
@@ -98,11 +98,11 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
|
|
|
98
98
|
// Lightweight debug endpoint for Sequencer metrics
|
|
99
99
|
if (url.pathname === '/debug/sequencer' && request.method === 'GET') {
|
|
100
100
|
try {
|
|
101
|
-
if (!('
|
|
101
|
+
if (!('ALTERAN_SEQUENCER' in resolvedEnv) || !resolvedEnv.ALTERAN_SEQUENCER) {
|
|
102
102
|
return new Response('Sequencer not configured', { status: 503 }) as unknown as WorkersResponse;
|
|
103
103
|
}
|
|
104
|
-
const id = (resolvedEnv as any).
|
|
105
|
-
const stub = (resolvedEnv as any).
|
|
104
|
+
const id = (resolvedEnv as any).ALTERAN_SEQUENCER.idFromName('default');
|
|
105
|
+
const stub = (resolvedEnv as any).ALTERAN_SEQUENCER.get(id);
|
|
106
106
|
const proxyRequest = new Request(new URL('/metrics', request.url).toString(), { method: 'GET' });
|
|
107
107
|
const response = await stub.fetch(proxyRequest as any);
|
|
108
108
|
// Pass through JSON
|
|
@@ -130,21 +130,43 @@ export function createPdsFetchHandler(options?: CreatePdsFetchHandlerOptions): P
|
|
|
130
130
|
}
|
|
131
131
|
return new Response('This endpoint requires a WebSocket (wss://) upgrade', { status: 426 }) as unknown as WorkersResponse;
|
|
132
132
|
}
|
|
133
|
-
if (!resolvedEnv.
|
|
133
|
+
if (!resolvedEnv.ALTERAN_SEQUENCER) {
|
|
134
134
|
return new Response('Sequencer not configured', { status: 503 }) as unknown as WorkersResponse;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
const id = resolvedEnv.
|
|
138
|
-
const stub = resolvedEnv.
|
|
137
|
+
const id = resolvedEnv.ALTERAN_SEQUENCER.idFromName('default');
|
|
138
|
+
const stub = resolvedEnv.ALTERAN_SEQUENCER.get(id);
|
|
139
139
|
return (await stub.fetch(request as any)) as unknown as WorkersResponse;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
const astroFetch = await getAstroFetch(options);
|
|
143
|
-
const response = await astroFetch(request, resolvedEnv as any, ctx);
|
|
143
|
+
const response = await astroFetch(normalizeXrpcRequestForAstro(request), resolvedEnv as any, ctx);
|
|
144
144
|
return response as unknown as WorkersResponse;
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
export function normalizeXrpcRequestForAstro(request: WorkersRequest): WorkersRequest {
|
|
149
|
+
const url = new URL(request.url);
|
|
150
|
+
if (!url.pathname.startsWith('/xrpc/')) {
|
|
151
|
+
return request;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Astro's SSR origin-check middleware rejects unsafe requests when Origin is
|
|
155
|
+
// absent or cross-origin. XRPC is a bearer-token API, not cookie/form auth,
|
|
156
|
+
// and atproto clients legitimately send bodyless POSTs from native runtimes.
|
|
157
|
+
if (request.headers.get('origin') === url.origin) {
|
|
158
|
+
return request;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const headerRecord: Record<string, string> = {};
|
|
162
|
+
request.headers.forEach((value, key) => {
|
|
163
|
+
headerRecord[key] = value;
|
|
164
|
+
});
|
|
165
|
+
headerRecord.origin = url.origin;
|
|
166
|
+
|
|
167
|
+
return new Request(request as any, { headers: headerRecord }) as unknown as WorkersRequest;
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
type AstroFetchHandler = (
|
|
149
171
|
request: WorkersRequest,
|
|
150
172
|
env: Env,
|
package/src/worker/sequencer.ts
CHANGED
|
@@ -46,7 +46,7 @@ export class Sequencer {
|
|
|
46
46
|
constructor(state: DurableObjectState, env: Env) {
|
|
47
47
|
this.state = state as HibernatableState;
|
|
48
48
|
this.env = env;
|
|
49
|
-
this.db = env.
|
|
49
|
+
this.db = env.ALTERAN_DB;
|
|
50
50
|
this.maxWindow = parseInt(env.PDS_SEQ_WINDOW || '512', 10);
|
|
51
51
|
|
|
52
52
|
// Reconcile nextSeq with DB on construction so replay and append agree
|
package/types/env.d.ts
CHANGED
|
@@ -16,9 +16,9 @@ export interface SecretsStoreSecret {
|
|
|
16
16
|
|
|
17
17
|
declare global {
|
|
18
18
|
interface Env {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
ALTERAN_DB: D1Database;
|
|
20
|
+
ALTERAN_BLOBS: R2Bucket;
|
|
21
|
+
ALTERAN_SEQUENCER?: DurableObjectNamespace;
|
|
22
22
|
ASSETS?: {
|
|
23
23
|
fetch: (req: Request | string) => Promise<Response>;
|
|
24
24
|
};
|