@alteran/astro 0.1.13 → 0.3.0
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 +28 -3
- package/index.js +2 -4
- package/migrations/0006_adorable_spectrum.sql +11 -0
- package/migrations/meta/0006_snapshot.json +429 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +6 -3
- package/src/db/account.ts +145 -0
- package/src/db/dal.ts +27 -9
- package/src/db/repo.ts +9 -8
- package/src/db/schema.ts +29 -11
- package/src/lib/actor.ts +133 -0
- package/src/lib/appview.ts +508 -0
- package/src/lib/auth.ts +26 -3
- package/src/lib/blob-refs.ts +9 -13
- package/src/lib/chat.ts +238 -0
- package/src/lib/config.ts +15 -7
- package/src/lib/feed.ts +165 -0
- package/src/lib/jwt.ts +144 -47
- package/src/lib/labeler.ts +91 -0
- package/src/lib/mst/blockstore.ts +98 -14
- package/src/lib/password.ts +40 -0
- package/src/lib/preferences.ts +73 -0
- package/src/lib/relay.ts +101 -0
- package/src/lib/secrets.ts +4 -1
- package/src/lib/session-tokens.ts +202 -0
- package/src/lib/token-cleanup.ts +3 -12
- package/src/lib/util.ts +17 -2
- package/src/middleware.ts +20 -21
- package/src/pages/.well-known/did.json.ts +45 -32
- package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +23 -0
- package/src/pages/xrpc/app.bsky.actor.getProfile.ts +34 -0
- package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +42 -0
- package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +36 -0
- package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +42 -0
- package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +37 -0
- package/src/pages/xrpc/app.bsky.feed.getPosts.ts +26 -0
- package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +35 -0
- package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +29 -0
- package/src/pages/xrpc/app.bsky.graph.getFollows.ts +29 -0
- package/src/pages/xrpc/app.bsky.labeler.getServices.ts +29 -0
- package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +20 -0
- package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +27 -0
- package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +19 -0
- package/src/pages/xrpc/app.bsky.unspecced.getConfig.ts +15 -0
- package/src/pages/xrpc/chat.bsky.convo.getLog.ts +26 -0
- package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +37 -0
- package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +64 -66
- package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +24 -0
- package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +127 -0
- package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +91 -0
- package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +6 -2
- package/src/pages/xrpc/com.atproto.server.createSession.ts +36 -8
- package/src/pages/xrpc/com.atproto.server.describeServer.ts +37 -4
- package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +64 -0
- package/src/pages/xrpc/com.atproto.server.refreshSession.ts +55 -32
- package/src/services/repo-manager.ts +15 -6
- package/src/worker/runtime.ts +9 -0
- package/types/env.d.ts +10 -1
- package/src/pages/xrpc/com.atproto.repo.importRepo.ts +0 -142
- package/src/pages/xrpc/com.atproto.server.activateAccount.ts +0 -53
- package/src/pages/xrpc/com.atproto.server.createAccount.ts +0 -99
- package/src/pages/xrpc/com.atproto.server.deactivateAccount.ts +0 -53
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { eq, or, lt } from 'drizzle-orm';
|
|
2
|
+
import { getDb } from './client';
|
|
3
|
+
import { account, refresh_token_store, secret } from './schema';
|
|
4
|
+
import type { Env } from '../env';
|
|
5
|
+
import { normalizeHandle } from '../lib/handle';
|
|
6
|
+
|
|
7
|
+
const NOW = () => Math.floor(Date.now());
|
|
8
|
+
|
|
9
|
+
export type AccountRow = typeof account.$inferSelect;
|
|
10
|
+
export type RefreshTokenRow = typeof refresh_token_store.$inferSelect;
|
|
11
|
+
|
|
12
|
+
function normalizeIdentifier(identifier: string): { did: string | null; handle: string | null } {
|
|
13
|
+
if (!identifier) return { did: null, handle: null };
|
|
14
|
+
const trimmed = identifier.trim();
|
|
15
|
+
if (trimmed.startsWith('did:')) {
|
|
16
|
+
return { did: trimmed, handle: null };
|
|
17
|
+
}
|
|
18
|
+
return { did: null, handle: normalizeHandle(trimmed) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getAccountByIdentifier(env: Env, identifier: string): Promise<AccountRow | null> {
|
|
22
|
+
const db = getDb(env);
|
|
23
|
+
const ident = normalizeIdentifier(identifier);
|
|
24
|
+
const clauses = [] as any[];
|
|
25
|
+
if (ident.did) clauses.push(eq(account.did, ident.did));
|
|
26
|
+
if (ident.handle) clauses.push(eq(account.handle, ident.handle));
|
|
27
|
+
if (clauses.length === 0) return null;
|
|
28
|
+
const where = clauses.length === 1 ? clauses[0] : or(...clauses);
|
|
29
|
+
return db.select().from(account).where(where).get();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function createAccount(env: Env, data: {
|
|
33
|
+
did: string;
|
|
34
|
+
handle: string;
|
|
35
|
+
passwordScrypt: string | null;
|
|
36
|
+
email?: string | null;
|
|
37
|
+
}): Promise<void> {
|
|
38
|
+
const db = getDb(env);
|
|
39
|
+
const now = NOW();
|
|
40
|
+
await db
|
|
41
|
+
.insert(account)
|
|
42
|
+
.values({
|
|
43
|
+
did: data.did,
|
|
44
|
+
handle: normalizeHandle(data.handle),
|
|
45
|
+
passwordScrypt: data.passwordScrypt ?? null,
|
|
46
|
+
email: data.email ?? null,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
})
|
|
50
|
+
.onConflictDoUpdate({
|
|
51
|
+
target: account.did,
|
|
52
|
+
set: {
|
|
53
|
+
handle: normalizeHandle(data.handle),
|
|
54
|
+
passwordScrypt: data.passwordScrypt ?? null,
|
|
55
|
+
email: data.email ?? null,
|
|
56
|
+
updatedAt: now,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function updateAccountPassword(env: Env, did: string, passwordScrypt: string): Promise<void> {
|
|
62
|
+
const db = getDb(env);
|
|
63
|
+
await db
|
|
64
|
+
.update(account)
|
|
65
|
+
.set({ passwordScrypt, updatedAt: NOW() })
|
|
66
|
+
.where(eq(account.did, did))
|
|
67
|
+
.run();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function storeRefreshToken(env: Env, data: {
|
|
71
|
+
id: string;
|
|
72
|
+
did: string;
|
|
73
|
+
expiresAt: number; // epoch seconds
|
|
74
|
+
appPasswordName?: string | null;
|
|
75
|
+
}): Promise<void> {
|
|
76
|
+
const db = getDb(env);
|
|
77
|
+
await db
|
|
78
|
+
.insert(refresh_token_store)
|
|
79
|
+
.values({
|
|
80
|
+
id: data.id,
|
|
81
|
+
did: data.did,
|
|
82
|
+
expiresAt: data.expiresAt,
|
|
83
|
+
appPasswordName: data.appPasswordName ?? null,
|
|
84
|
+
nextId: null,
|
|
85
|
+
})
|
|
86
|
+
.onConflictDoUpdate({
|
|
87
|
+
target: refresh_token_store.id,
|
|
88
|
+
set: {
|
|
89
|
+
did: data.did,
|
|
90
|
+
expiresAt: data.expiresAt,
|
|
91
|
+
appPasswordName: data.appPasswordName ?? null,
|
|
92
|
+
nextId: null,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export async function markRefreshTokenRotated(env: Env, id: string, nextId: string, graceExpiresAt: number): Promise<void> {
|
|
98
|
+
const db = getDb(env);
|
|
99
|
+
await db
|
|
100
|
+
.update(refresh_token_store)
|
|
101
|
+
.set({ nextId, expiresAt: graceExpiresAt })
|
|
102
|
+
.where(eq(refresh_token_store.id, id))
|
|
103
|
+
.run();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function getRefreshToken(env: Env, id: string): Promise<RefreshTokenRow | null> {
|
|
107
|
+
const db = getDb(env);
|
|
108
|
+
return db.select().from(refresh_token_store).where(eq(refresh_token_store.id, id)).get();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function deleteRefreshToken(env: Env, id: string): Promise<void> {
|
|
112
|
+
const db = getDb(env);
|
|
113
|
+
await db.delete(refresh_token_store).where(eq(refresh_token_store.id, id)).run();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function cleanupExpiredRefreshTokens(env: Env, now: number): Promise<number> {
|
|
117
|
+
const db = getDb(env);
|
|
118
|
+
const res = await db.delete(refresh_token_store).where(lt(refresh_token_store.expiresAt, now)).run();
|
|
119
|
+
return res.meta.changes ?? 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function getSecret(env: Env, key: string): Promise<string | null> {
|
|
123
|
+
const db = getDb(env);
|
|
124
|
+
const row = await db.select().from(secret).where(eq(secret.key, key)).get();
|
|
125
|
+
return row?.value ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function setSecret(env: Env, key: string, value: string): Promise<void> {
|
|
129
|
+
const db = getDb(env);
|
|
130
|
+
await db
|
|
131
|
+
.insert(secret)
|
|
132
|
+
.values({ key, value, updatedAt: NOW() })
|
|
133
|
+
.onConflictDoUpdate({
|
|
134
|
+
target: secret.key,
|
|
135
|
+
set: { value, updatedAt: NOW() },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export async function getOrCreateSecret(env: Env, key: string, factory: () => Promise<string>): Promise<string> {
|
|
140
|
+
const existing = await getSecret(env, key);
|
|
141
|
+
if (existing) return existing;
|
|
142
|
+
const value = await factory();
|
|
143
|
+
await setSecret(env, key, value);
|
|
144
|
+
return value;
|
|
145
|
+
}
|
package/src/db/dal.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { getDb } from './client';
|
|
2
2
|
import { record, type NewRecordRow, blob_ref, blob_usage, blob_quota } from './schema';
|
|
3
3
|
import type { Env } from '../env';
|
|
4
|
-
import { eq, inArray, and } from 'drizzle-orm';
|
|
4
|
+
import { eq, inArray, and, sql } from 'drizzle-orm';
|
|
5
5
|
|
|
6
6
|
export async function putRecord(env: Env, row: NewRecordRow) {
|
|
7
7
|
const db = getDb(env);
|
|
8
|
-
await db.insert(record).values(row).onConflictDoUpdate({
|
|
8
|
+
await db.insert(record).values(row).onConflictDoUpdate({
|
|
9
|
+
target: record.uri,
|
|
10
|
+
set: {
|
|
11
|
+
cid: sql.raw(`excluded.${record.cid.name}`),
|
|
12
|
+
json: sql.raw(`excluded.${record.json.name}`)
|
|
13
|
+
}
|
|
14
|
+
});
|
|
9
15
|
}
|
|
10
16
|
|
|
11
17
|
export async function getRecord(env: Env, uri: string) {
|
|
@@ -35,7 +41,15 @@ export async function putBlobRef(env: Env, did: string, cid: string, key: string
|
|
|
35
41
|
await db
|
|
36
42
|
.insert(blob_ref)
|
|
37
43
|
.values({ did, cid, key, mime, size })
|
|
38
|
-
.onConflictDoUpdate({
|
|
44
|
+
.onConflictDoUpdate({
|
|
45
|
+
target: blob_ref.cid,
|
|
46
|
+
set: {
|
|
47
|
+
did: sql.raw(`excluded.${blob_ref.did.name}`),
|
|
48
|
+
key: sql.raw(`excluded.${blob_ref.key.name}`),
|
|
49
|
+
mime: sql.raw(`excluded.${blob_ref.mime.name}`),
|
|
50
|
+
size: sql.raw(`excluded.${blob_ref.size.name}`)
|
|
51
|
+
}
|
|
52
|
+
});
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
export async function setRecordBlobUsage(env: Env, uri: string, keys: string[]) {
|
|
@@ -71,20 +85,24 @@ export async function updateBlobQuota(env: Env, did: string, bytesAdded: number,
|
|
|
71
85
|
const db = getDb(env);
|
|
72
86
|
const current = await getBlobQuota(env, did);
|
|
73
87
|
|
|
88
|
+
const newTotalBytes = current.total_bytes + bytesAdded;
|
|
89
|
+
const newBlobCount = current.blob_count + countAdded;
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
|
|
74
92
|
await db
|
|
75
93
|
.insert(blob_quota)
|
|
76
94
|
.values({
|
|
77
95
|
did,
|
|
78
|
-
total_bytes:
|
|
79
|
-
blob_count:
|
|
80
|
-
updated_at:
|
|
96
|
+
total_bytes: newTotalBytes,
|
|
97
|
+
blob_count: newBlobCount,
|
|
98
|
+
updated_at: now,
|
|
81
99
|
})
|
|
82
100
|
.onConflictDoUpdate({
|
|
83
101
|
target: blob_quota.did,
|
|
84
102
|
set: {
|
|
85
|
-
total_bytes:
|
|
86
|
-
blob_count:
|
|
87
|
-
updated_at:
|
|
103
|
+
total_bytes: sql.raw(`excluded.${blob_quota.total_bytes.name}`),
|
|
104
|
+
blob_count: sql.raw(`excluded.${blob_quota.blob_count.name}`),
|
|
105
|
+
updated_at: sql.raw(`excluded.${blob_quota.updated_at.name}`),
|
|
88
106
|
},
|
|
89
107
|
});
|
|
90
108
|
}
|
package/src/db/repo.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { Env } from '../env';
|
|
2
2
|
import { drizzle } from 'drizzle-orm/d1';
|
|
3
|
-
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { eq, sql } from 'drizzle-orm';
|
|
4
4
|
import { repo_root, commit_log } from './schema';
|
|
5
5
|
import { RepoManager } from '../services/repo-manager';
|
|
6
6
|
import { createCommit, signCommit, commitCid, generateTid, serializeCommit } from '../lib/commit';
|
|
7
7
|
import { CID } from 'multiformats/cid';
|
|
8
|
+
import { resolveSecret } from '../lib/secrets';
|
|
8
9
|
|
|
9
10
|
export async function getRoot(env: Env) {
|
|
10
11
|
const db = drizzle(env.DB);
|
|
11
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
12
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
12
13
|
return db.select().from(repo_root).where(eq(repo_root.did, did)).get();
|
|
13
14
|
}
|
|
14
15
|
|
|
@@ -22,7 +23,7 @@ export async function bumpRoot(env: Env, prevMstRoot?: CID): Promise<{
|
|
|
22
23
|
mstRoot: CID;
|
|
23
24
|
}> {
|
|
24
25
|
const db = drizzle(env.DB);
|
|
25
|
-
const did = env.PDS_DID ?? 'did:example:single-user';
|
|
26
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
26
27
|
|
|
27
28
|
// Resolve signing key (use ephemeral dev key if not configured and not production)
|
|
28
29
|
const signingKey = await getSigningKey(env);
|
|
@@ -54,19 +55,19 @@ export async function bumpRoot(env: Env, prevMstRoot?: CID): Promise<{
|
|
|
54
55
|
const cid = await commitCid(signedCommit);
|
|
55
56
|
const cidString = cid.toString();
|
|
56
57
|
|
|
57
|
-
// Update repo root
|
|
58
|
+
// Update repo root - use sql.raw with excluded to properly reference INSERT values
|
|
58
59
|
await db
|
|
59
60
|
.insert(repo_root)
|
|
60
61
|
.values({
|
|
61
62
|
did,
|
|
62
63
|
commitCid: cidString,
|
|
63
|
-
rev
|
|
64
|
+
rev, // Store TID as text
|
|
64
65
|
})
|
|
65
66
|
.onConflictDoUpdate({
|
|
66
67
|
target: repo_root.did,
|
|
67
68
|
set: {
|
|
68
|
-
commitCid:
|
|
69
|
-
rev:
|
|
69
|
+
commitCid: sql.raw('excluded.commit_cid'),
|
|
70
|
+
rev: sql.raw('excluded.rev'),
|
|
70
71
|
},
|
|
71
72
|
})
|
|
72
73
|
.run();
|
|
@@ -111,7 +112,7 @@ export async function appendCommit(env: Env, cid: string, rev: string, data: str
|
|
|
111
112
|
let cachedDevSigningKey: string | undefined;
|
|
112
113
|
|
|
113
114
|
async function getSigningKey(env: Env): Promise<string> {
|
|
114
|
-
const configured = env.REPO_SIGNING_KEY;
|
|
115
|
+
const configured = await resolveSecret(env.REPO_SIGNING_KEY);
|
|
115
116
|
if (configured && configured.trim() !== '') return configured;
|
|
116
117
|
|
|
117
118
|
const envName = (env as any).ENVIRONMENT || 'development';
|
package/src/db/schema.ts
CHANGED
|
@@ -1,9 +1,36 @@
|
|
|
1
|
-
import { sqliteTable, text, integer, index, primaryKey } from 'drizzle-orm/sqlite-core';
|
|
1
|
+
import { sqliteTable, text, integer, index, primaryKey, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
|
2
|
+
|
|
3
|
+
export const secret = sqliteTable('secret', {
|
|
4
|
+
key: text('key').primaryKey().notNull(),
|
|
5
|
+
value: text('value').notNull(),
|
|
6
|
+
updatedAt: integer('updated_at', { mode: 'number' }).notNull(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const account = sqliteTable('account', {
|
|
10
|
+
did: text('did').primaryKey().notNull(),
|
|
11
|
+
handle: text('handle').notNull(),
|
|
12
|
+
passwordScrypt: text('password_scrypt'),
|
|
13
|
+
email: text('email'),
|
|
14
|
+
createdAt: integer('created_at', { mode: 'number' }).notNull(),
|
|
15
|
+
updatedAt: integer('updated_at', { mode: 'number' }).notNull(),
|
|
16
|
+
}, (table) => ({
|
|
17
|
+
handleIdx: uniqueIndex('account_handle_unique').on(table.handle),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
export const refresh_token_store = sqliteTable('refresh_token', {
|
|
21
|
+
id: text('id').primaryKey().notNull(),
|
|
22
|
+
did: text('did').notNull(),
|
|
23
|
+
expiresAt: integer('expires_at', { mode: 'number' }).notNull(),
|
|
24
|
+
appPasswordName: text('app_password_name'),
|
|
25
|
+
nextId: text('next_id'),
|
|
26
|
+
}, (table) => ({
|
|
27
|
+
didIdx: index('refresh_token_did_idx').on(table.did),
|
|
28
|
+
}));
|
|
2
29
|
|
|
3
30
|
export const repo_root = sqliteTable('repo_root', {
|
|
4
31
|
did: text('did').primaryKey().notNull(),
|
|
5
32
|
commitCid: text('commit_cid').notNull(),
|
|
6
|
-
rev:
|
|
33
|
+
rev: text('rev').notNull(), // TID format (e.g., "3m2biurz7cl27")
|
|
7
34
|
});
|
|
8
35
|
|
|
9
36
|
export const record = sqliteTable('record', {
|
|
@@ -61,15 +88,6 @@ export const blockstore = sqliteTable('blockstore', {
|
|
|
61
88
|
bytes: text('bytes'),
|
|
62
89
|
});
|
|
63
90
|
|
|
64
|
-
export const token_revocation = sqliteTable('token_revocation', {
|
|
65
|
-
jti: text('jti').primaryKey().notNull(),
|
|
66
|
-
exp: integer('exp').notNull(),
|
|
67
|
-
revoked_at: integer('revoked_at').notNull(),
|
|
68
|
-
}, (table) => ({
|
|
69
|
-
// Index for cleanup queries (finding expired tokens)
|
|
70
|
-
expIdx: index('token_revocation_exp_idx').on(table.exp),
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
91
|
export const login_attempts = sqliteTable('login_attempts', {
|
|
74
92
|
ip: text('ip').primaryKey().notNull(),
|
|
75
93
|
attempts: integer('attempts').notNull().default(0),
|
package/src/lib/actor.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { getDb } from '../db/client';
|
|
2
|
+
import { record } from '../db/schema';
|
|
3
|
+
import { resolveSecret } from './secrets';
|
|
4
|
+
import type { Env } from '../env';
|
|
5
|
+
import { eq } from 'drizzle-orm';
|
|
6
|
+
|
|
7
|
+
interface ProfileRecord {
|
|
8
|
+
displayName?: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
pronouns?: string;
|
|
11
|
+
website?: string;
|
|
12
|
+
avatar?: string;
|
|
13
|
+
banner?: string;
|
|
14
|
+
joinedViaStarterPack?: any;
|
|
15
|
+
pinnedPost?: any;
|
|
16
|
+
labels?: any;
|
|
17
|
+
createdAt?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PrimaryActor {
|
|
21
|
+
did: string;
|
|
22
|
+
handle: string;
|
|
23
|
+
displayName?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
pronouns?: string;
|
|
26
|
+
website?: string;
|
|
27
|
+
avatar?: string;
|
|
28
|
+
banner?: string;
|
|
29
|
+
labels?: any;
|
|
30
|
+
createdAt?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const PROFILE_COLLECTION = 'app.bsky.actor.profile';
|
|
34
|
+
|
|
35
|
+
export async function fetchProfileRecord(env: Env, did: string): Promise<ProfileRecord | null> {
|
|
36
|
+
const db = getDb(env);
|
|
37
|
+
|
|
38
|
+
const targetUri = `at://${did}/${PROFILE_COLLECTION}/self`;
|
|
39
|
+
const byDid = await db.select().from(record).where(eq(record.uri, targetUri)).get();
|
|
40
|
+
if (byDid?.json) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(byDid.json) as ProfileRecord;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Fallback: pick the most recent profile record regardless of DID
|
|
49
|
+
const fallback = await env.DB.prepare(
|
|
50
|
+
'SELECT json FROM record WHERE uri LIKE ? ORDER BY rowid DESC LIMIT 1'
|
|
51
|
+
)
|
|
52
|
+
.bind(`%/${PROFILE_COLLECTION}/%`)
|
|
53
|
+
.first<{ json: string }>();
|
|
54
|
+
|
|
55
|
+
if (fallback?.json) {
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(fallback.json) as ProfileRecord;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getPrimaryActor(env: Env): Promise<PrimaryActor> {
|
|
67
|
+
const did = (await resolveSecret(env.PDS_DID)) ?? 'did:example:single-user';
|
|
68
|
+
const handle = (await resolveSecret(env.PDS_HANDLE)) ?? 'user.example.com';
|
|
69
|
+
|
|
70
|
+
const profile = await fetchProfileRecord(env, did);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
did,
|
|
74
|
+
handle,
|
|
75
|
+
displayName: profile?.displayName ?? handle,
|
|
76
|
+
description: profile?.description,
|
|
77
|
+
pronouns: profile?.pronouns,
|
|
78
|
+
website: profile?.website,
|
|
79
|
+
avatar: profile?.avatar,
|
|
80
|
+
banner: profile?.banner,
|
|
81
|
+
labels: profile?.labels,
|
|
82
|
+
createdAt: profile?.createdAt,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function matchesPrimaryActor(identifier: string | null | undefined, actor: PrimaryActor): boolean {
|
|
87
|
+
if (!identifier) return false;
|
|
88
|
+
const lower = identifier.toLowerCase();
|
|
89
|
+
return lower === actor.did.toLowerCase() || lower === actor.handle.toLowerCase();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function buildProfileViewBasic(actor: PrimaryActor) {
|
|
93
|
+
const createdAt = actor.createdAt ?? new Date().toISOString();
|
|
94
|
+
const labels = Array.isArray(actor.labels) ? actor.labels : [];
|
|
95
|
+
return {
|
|
96
|
+
$type: 'app.bsky.actor.defs#profileViewBasic',
|
|
97
|
+
did: actor.did,
|
|
98
|
+
handle: actor.handle,
|
|
99
|
+
displayName: actor.displayName,
|
|
100
|
+
pronouns: actor.pronouns,
|
|
101
|
+
avatar: actor.avatar,
|
|
102
|
+
createdAt,
|
|
103
|
+
associated: { $type: 'app.bsky.actor.defs#profileAssociated' },
|
|
104
|
+
labels,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function buildProfileView(actor: PrimaryActor) {
|
|
109
|
+
const basic = buildProfileViewBasic(actor);
|
|
110
|
+
return {
|
|
111
|
+
...basic,
|
|
112
|
+
$type: 'app.bsky.actor.defs#profileView',
|
|
113
|
+
description: actor.description,
|
|
114
|
+
indexedAt: actor.createdAt ?? new Date().toISOString(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function buildProfileViewDetailed(actor: PrimaryActor, counts: {
|
|
119
|
+
followers: number;
|
|
120
|
+
follows: number;
|
|
121
|
+
posts: number;
|
|
122
|
+
}) {
|
|
123
|
+
const view = buildProfileView(actor);
|
|
124
|
+
return {
|
|
125
|
+
...view,
|
|
126
|
+
$type: 'app.bsky.actor.defs#profileViewDetailed',
|
|
127
|
+
banner: actor.banner,
|
|
128
|
+
website: actor.website,
|
|
129
|
+
followersCount: counts.followers,
|
|
130
|
+
followsCount: counts.follows,
|
|
131
|
+
postsCount: counts.posts,
|
|
132
|
+
};
|
|
133
|
+
}
|