@alteran/astro 0.1.14 → 0.3.1

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.
Files changed (62) hide show
  1. package/README.md +35 -10
  2. package/index.js +2 -4
  3. package/migrations/0006_adorable_spectrum.sql +11 -0
  4. package/migrations/meta/0006_snapshot.json +429 -0
  5. package/migrations/meta/_journal.json +7 -0
  6. package/package.json +7 -3
  7. package/src/db/account.ts +145 -0
  8. package/src/db/dal.ts +27 -9
  9. package/src/db/repo.ts +9 -8
  10. package/src/db/schema.ts +29 -11
  11. package/src/lib/actor.ts +133 -0
  12. package/src/lib/appview.ts +508 -0
  13. package/src/lib/auth.ts +22 -2
  14. package/src/lib/blob-refs.ts +9 -13
  15. package/src/lib/chat.ts +238 -0
  16. package/src/lib/config.ts +15 -7
  17. package/src/lib/feed.ts +165 -0
  18. package/src/lib/jwt.ts +231 -79
  19. package/src/lib/labeler.ts +91 -0
  20. package/src/lib/mst/blockstore.ts +98 -14
  21. package/src/lib/password.ts +40 -0
  22. package/src/lib/preferences.ts +73 -0
  23. package/src/lib/relay.ts +101 -0
  24. package/src/lib/secrets.ts +29 -21
  25. package/src/lib/session-tokens.ts +202 -0
  26. package/src/lib/token-cleanup.ts +3 -12
  27. package/src/lib/util.ts +17 -2
  28. package/src/middleware.ts +20 -21
  29. package/src/pages/.well-known/did.json.ts +45 -32
  30. package/src/pages/xrpc/app.bsky.actor.getPreferences.ts +23 -0
  31. package/src/pages/xrpc/app.bsky.actor.getProfile.ts +34 -0
  32. package/src/pages/xrpc/app.bsky.actor.getProfiles.ts +42 -0
  33. package/src/pages/xrpc/app.bsky.actor.putPreferences.ts +36 -0
  34. package/src/pages/xrpc/app.bsky.feed.getAuthorFeed.ts +42 -0
  35. package/src/pages/xrpc/app.bsky.feed.getPostThread.ts +37 -0
  36. package/src/pages/xrpc/app.bsky.feed.getPosts.ts +26 -0
  37. package/src/pages/xrpc/app.bsky.feed.getTimeline.ts +35 -0
  38. package/src/pages/xrpc/app.bsky.graph.getFollowers.ts +29 -0
  39. package/src/pages/xrpc/app.bsky.graph.getFollows.ts +29 -0
  40. package/src/pages/xrpc/app.bsky.labeler.getServices.ts +29 -0
  41. package/src/pages/xrpc/app.bsky.notification.getUnreadCount.ts +20 -0
  42. package/src/pages/xrpc/app.bsky.notification.listNotifications.ts +27 -0
  43. package/src/pages/xrpc/app.bsky.unspecced.getAgeAssuranceState.ts +19 -0
  44. package/src/pages/xrpc/app.bsky.unspecced.getConfig.ts +15 -0
  45. package/src/pages/xrpc/chat.bsky.convo.getLog.ts +26 -0
  46. package/src/pages/xrpc/chat.bsky.convo.listConvos.ts +37 -0
  47. package/src/pages/xrpc/com.atproto.identity.getRecommendedDidCredentials.ts +64 -66
  48. package/src/pages/xrpc/com.atproto.identity.requestPlcOperationSignature.ts +24 -0
  49. package/src/pages/xrpc/com.atproto.identity.signPlcOperation.ts +127 -0
  50. package/src/pages/xrpc/com.atproto.identity.submitPlcOperation.ts +91 -0
  51. package/src/pages/xrpc/com.atproto.repo.uploadBlob.ts +6 -2
  52. package/src/pages/xrpc/com.atproto.server.createSession.ts +36 -8
  53. package/src/pages/xrpc/com.atproto.server.describeServer.ts +37 -4
  54. package/src/pages/xrpc/com.atproto.server.getServiceAuth.ts +64 -0
  55. package/src/pages/xrpc/com.atproto.server.refreshSession.ts +55 -32
  56. package/src/services/repo-manager.ts +15 -6
  57. package/src/worker/runtime.ts +21 -0
  58. package/types/env.d.ts +11 -2
  59. package/src/pages/xrpc/com.atproto.repo.importRepo.ts +0 -142
  60. package/src/pages/xrpc/com.atproto.server.activateAccount.ts +0 -53
  61. package/src/pages/xrpc/com.atproto.server.createAccount.ts +0 -99
  62. 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({ target: record.uri, set: { cid: row.cid, json: row.json } });
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({ target: blob_ref.cid, set: { did, key, mime, size } });
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: current.total_bytes + bytesAdded,
79
- blob_count: current.blob_count + countAdded,
80
- updated_at: Date.now(),
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: current.total_bytes + bytesAdded,
86
- blob_count: current.blob_count + countAdded,
87
- updated_at: Date.now(),
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: parseInt(rev, 36), // Convert TID to number for compatibility
64
+ rev, // Store TID as text
64
65
  })
65
66
  .onConflictDoUpdate({
66
67
  target: repo_root.did,
67
68
  set: {
68
- commitCid: cidString,
69
- rev: parseInt(rev, 36),
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: integer('rev').notNull(),
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),
@@ -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
+ }