@takosjp/yurucommu-core 3.0.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.
Files changed (185) hide show
  1. package/LICENSE +16 -0
  2. package/README.md +82 -0
  3. package/migrations/0001_init.sql +495 -0
  4. package/migrations/0002_social_remote_actor_edges.sql +92 -0
  5. package/migrations/0003_activity_remote_object_edges.sql +68 -0
  6. package/migrations/0004_blocklist.sql +26 -0
  7. package/migrations/0005_story_community_scope.sql +13 -0
  8. package/migrations/0006_dm_community_read_status.sql +19 -0
  9. package/migrations/0007_moderation_reports.sql +22 -0
  10. package/migrations/0008_actor_fields_aka.sql +18 -0
  11. package/migrations/0009_object_tags.sql +13 -0
  12. package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
  13. package/migrations/0011_drop_remote_actor_fks.sql +205 -0
  14. package/migrations/0012_objects_content_fts.sql +39 -0
  15. package/migrations/0013_efficiency_indexes.sql +13 -0
  16. package/migrations/0014_inbox_actor_created_idx.sql +15 -0
  17. package/migrations/0015_community_bans.sql +16 -0
  18. package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
  19. package/migrations/0017_mobile_push_registrations.sql +22 -0
  20. package/migrations/README.md +122 -0
  21. package/package.json +75 -0
  22. package/packages/api/LICENSE +16 -0
  23. package/packages/api/package.json +30 -0
  24. package/packages/api/src/index.ts +4 -0
  25. package/packages/api/src/lib/api/account.ts +20 -0
  26. package/packages/api/src/lib/api/actors.ts +149 -0
  27. package/packages/api/src/lib/api/auth.ts +46 -0
  28. package/packages/api/src/lib/api/communities.ts +329 -0
  29. package/packages/api/src/lib/api/dm.test.ts +67 -0
  30. package/packages/api/src/lib/api/dm.ts +236 -0
  31. package/packages/api/src/lib/api/fetch.ts +111 -0
  32. package/packages/api/src/lib/api/follow.ts +30 -0
  33. package/packages/api/src/lib/api/media.ts +100 -0
  34. package/packages/api/src/lib/api/moderation.ts +98 -0
  35. package/packages/api/src/lib/api/normalize.ts +71 -0
  36. package/packages/api/src/lib/api/notifications.test.ts +63 -0
  37. package/packages/api/src/lib/api/notifications.ts +61 -0
  38. package/packages/api/src/lib/api/posts.test.ts +110 -0
  39. package/packages/api/src/lib/api/posts.ts +181 -0
  40. package/packages/api/src/lib/api/recommendations.ts +22 -0
  41. package/packages/api/src/lib/api/search.ts +88 -0
  42. package/packages/api/src/lib/api/stories.ts +80 -0
  43. package/packages/api/src/lib/api.ts +15 -0
  44. package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
  45. package/packages/api/src/lib/transport.ts +40 -0
  46. package/packages/api/src/social-server.ts +47 -0
  47. package/packages/api/src/types/index.ts +185 -0
  48. package/scripts/apply-takosumi-migrations.ts +621 -0
  49. package/src/backend/federation-helpers.ts +36 -0
  50. package/src/backend/index.ts +872 -0
  51. package/src/backend/lib/account-migration.ts +106 -0
  52. package/src/backend/lib/activitypub-actor-cache.ts +238 -0
  53. package/src/backend/lib/activitypub-helpers.ts +131 -0
  54. package/src/backend/lib/activitypub-validators.ts +323 -0
  55. package/src/backend/lib/ap-context.ts +16 -0
  56. package/src/backend/lib/ap-ids.ts +101 -0
  57. package/src/backend/lib/ap-response.ts +30 -0
  58. package/src/backend/lib/ap-signing.ts +87 -0
  59. package/src/backend/lib/ap-verify.ts +670 -0
  60. package/src/backend/lib/auth-lockout.ts +230 -0
  61. package/src/backend/lib/backend-paths.ts +34 -0
  62. package/src/backend/lib/base64.ts +30 -0
  63. package/src/backend/lib/blocklist-purge.ts +109 -0
  64. package/src/backend/lib/blocklist.ts +279 -0
  65. package/src/backend/lib/chunk.ts +33 -0
  66. package/src/backend/lib/client-ip.ts +169 -0
  67. package/src/backend/lib/community-visibility.ts +230 -0
  68. package/src/backend/lib/crypto.ts +424 -0
  69. package/src/backend/lib/delivery/circuit.ts +265 -0
  70. package/src/backend/lib/delivery/metrics.ts +30 -0
  71. package/src/backend/lib/delivery/planner.ts +190 -0
  72. package/src/backend/lib/delivery/queue-batching.ts +626 -0
  73. package/src/backend/lib/delivery/queue-delivery.ts +641 -0
  74. package/src/backend/lib/delivery/queue.ts +576 -0
  75. package/src/backend/lib/delivery/transformers.ts +56 -0
  76. package/src/backend/lib/delivery/types.ts +139 -0
  77. package/src/backend/lib/errors.ts +114 -0
  78. package/src/backend/lib/federation-fetch.ts +296 -0
  79. package/src/backend/lib/feed-cursor.ts +57 -0
  80. package/src/backend/lib/feed-exclude.ts +48 -0
  81. package/src/backend/lib/hex.ts +8 -0
  82. package/src/backend/lib/log-mask.ts +213 -0
  83. package/src/backend/lib/logger.ts +285 -0
  84. package/src/backend/lib/mobile-contract.ts +137 -0
  85. package/src/backend/lib/oauth-providers.ts +324 -0
  86. package/src/backend/lib/oauth-utils.ts +148 -0
  87. package/src/backend/lib/oidc-id-token.ts +151 -0
  88. package/src/backend/lib/parse-helpers.ts +31 -0
  89. package/src/backend/lib/post-visibility.ts +190 -0
  90. package/src/backend/lib/session-actor.ts +61 -0
  91. package/src/backend/lib/ssrf.ts +428 -0
  92. package/src/backend/lib/strip-image-metadata.ts +191 -0
  93. package/src/backend/middleware/bearer-auth.ts +70 -0
  94. package/src/backend/middleware/body-limit.ts +212 -0
  95. package/src/backend/middleware/cache.ts +429 -0
  96. package/src/backend/middleware/csrf.ts +130 -0
  97. package/src/backend/middleware/error-handler.ts +77 -0
  98. package/src/backend/middleware/rate-limit.ts +308 -0
  99. package/src/backend/public.ts +21 -0
  100. package/src/backend/routes/account-teardown.ts +430 -0
  101. package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
  102. package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
  103. package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
  104. package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
  105. package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
  106. package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
  107. package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
  108. package/src/backend/routes/activitypub/inbox-types.ts +74 -0
  109. package/src/backend/routes/activitypub/inbox.ts +1191 -0
  110. package/src/backend/routes/activitypub/outbox.ts +0 -0
  111. package/src/backend/routes/activitypub/query-helpers.ts +227 -0
  112. package/src/backend/routes/activitypub.ts +616 -0
  113. package/src/backend/routes/actors-helpers.ts +487 -0
  114. package/src/backend/routes/actors.ts +1311 -0
  115. package/src/backend/routes/apps.ts +313 -0
  116. package/src/backend/routes/auth-helpers.ts +566 -0
  117. package/src/backend/routes/auth.ts +615 -0
  118. package/src/backend/routes/communities/membership-invites.ts +208 -0
  119. package/src/backend/routes/communities/membership-join.ts +335 -0
  120. package/src/backend/routes/communities/membership-members.ts +539 -0
  121. package/src/backend/routes/communities/membership-requests.ts +296 -0
  122. package/src/backend/routes/communities/membership-shared.ts +364 -0
  123. package/src/backend/routes/communities/messages.ts +479 -0
  124. package/src/backend/routes/communities/routes.ts +624 -0
  125. package/src/backend/routes/communities.ts +21 -0
  126. package/src/backend/routes/dm/contacts.ts +525 -0
  127. package/src/backend/routes/dm/conversations-helpers.ts +197 -0
  128. package/src/backend/routes/dm/conversations.ts +25 -0
  129. package/src/backend/routes/dm/messages.ts +658 -0
  130. package/src/backend/routes/dm/query-helpers.ts +85 -0
  131. package/src/backend/routes/dm/read-archive.ts +228 -0
  132. package/src/backend/routes/dm/requests.ts +222 -0
  133. package/src/backend/routes/dm/typing.ts +81 -0
  134. package/src/backend/routes/dm.ts +15 -0
  135. package/src/backend/routes/follow-helpers.ts +370 -0
  136. package/src/backend/routes/follow.ts +588 -0
  137. package/src/backend/routes/media.ts +692 -0
  138. package/src/backend/routes/mobile.ts +159 -0
  139. package/src/backend/routes/moderation.ts +373 -0
  140. package/src/backend/routes/notifications.ts +757 -0
  141. package/src/backend/routes/posts/delete-cascade.ts +330 -0
  142. package/src/backend/routes/posts/interactions.ts +795 -0
  143. package/src/backend/routes/posts/post-helpers.ts +847 -0
  144. package/src/backend/routes/posts/queries.ts +537 -0
  145. package/src/backend/routes/posts/routes.ts +865 -0
  146. package/src/backend/routes/posts/transformers.ts +161 -0
  147. package/src/backend/routes/posts.ts +17 -0
  148. package/src/backend/routes/recommendations.ts +88 -0
  149. package/src/backend/routes/search.ts +730 -0
  150. package/src/backend/routes/stories/interactions.ts +576 -0
  151. package/src/backend/routes/stories/query-helpers.ts +482 -0
  152. package/src/backend/routes/stories/routes.ts +906 -0
  153. package/src/backend/routes/stories.ts +13 -0
  154. package/src/backend/routes/takos-tools/dm.ts +249 -0
  155. package/src/backend/routes/takos-tools/follows.ts +225 -0
  156. package/src/backend/routes/takos-tools/posts.ts +292 -0
  157. package/src/backend/routes/takos-tools/search.ts +228 -0
  158. package/src/backend/routes/takos-tools/timeline.ts +132 -0
  159. package/src/backend/routes/takos-tools/types.ts +10 -0
  160. package/src/backend/routes/takos-tools-response.ts +178 -0
  161. package/src/backend/routes/takos-tools.ts +153 -0
  162. package/src/backend/routes/timeline.ts +755 -0
  163. package/src/backend/runtime/bun.ts +620 -0
  164. package/src/backend/runtime/cloudflare.ts +202 -0
  165. package/src/backend/runtime/compat-bun/types.ts +44 -0
  166. package/src/backend/runtime/memory-kv.ts +104 -0
  167. package/src/backend/runtime/shared.ts +142 -0
  168. package/src/backend/runtime/types.ts +205 -0
  169. package/src/backend/server.ts +636 -0
  170. package/src/backend/types.ts +143 -0
  171. package/src/db/index.ts +97 -0
  172. package/src/db/schema/actors.ts +129 -0
  173. package/src/db/schema/communities.ts +133 -0
  174. package/src/db/schema/date-utils.ts +17 -0
  175. package/src/db/schema/index.ts +17 -0
  176. package/src/db/schema/messaging.ts +241 -0
  177. package/src/db/schema/mobile.ts +37 -0
  178. package/src/db/schema/posts.ts +150 -0
  179. package/src/db/schema/relations.ts +266 -0
  180. package/src/db/schema/reports.ts +33 -0
  181. package/src/db/schema/social.ts +106 -0
  182. package/src/db/schema/stories.ts +70 -0
  183. package/src/db/schema.ts +15 -0
  184. package/src/plugin/public.ts +7 -0
  185. package/src/runtime/site-worker.ts +10 -0
@@ -0,0 +1,143 @@
1
+ import type { Database } from "../db/index.ts";
2
+ import type {
3
+ IKeyValueStore,
4
+ IObjectStorage,
5
+ IStaticAssets,
6
+ } from "./runtime/types.ts";
7
+ import type {
8
+ DeliveryDlqMessageV1,
9
+ DeliveryQueueMessageV1,
10
+ } from "./lib/delivery/types.ts";
11
+
12
+ /**
13
+ * Environment Variables (common across all runtimes)
14
+ */
15
+ export interface EnvVars {
16
+ APP_URL: string;
17
+
18
+ // Takos-specific endpoints are opt-in (fail-close by default).
19
+ ENABLE_TAKOS_TOOLS?: string;
20
+
21
+ // 認証設定(自由に組み合わせ可能)
22
+ AUTH_PASSWORD_HASH?: string; // PBKDF2-hashed password
23
+ GOOGLE_CLIENT_ID?: string;
24
+ GOOGLE_CLIENT_SECRET?: string;
25
+ X_CLIENT_ID?: string;
26
+ X_CLIENT_SECRET?: string;
27
+ OIDC_ISSUER_URL?: string;
28
+ OIDC_CLIENT_ID?: string;
29
+ OIDC_CLIENT_SECRET?: string;
30
+ OAUTH_ISSUER_URL?: string;
31
+ TAKOSUMI_ACCOUNTS_ISSUER_URL?: string;
32
+ TAKOSUMI_ACCOUNTS_CLIENT_ID?: string;
33
+ TAKOSUMI_ACCOUNTS_CLIENT_SECRET?: string;
34
+ // Pins the OIDC/OAuth subject allowed to take the single owner slot. When set,
35
+ // only a first-login whose subject equals this value becomes `owner`; any other
36
+ // first-login is refused. Prevents an owner-slot race on an OIDC-seeded Capsule.
37
+ OIDC_OWNER_SUB?: string;
38
+ TAKOSUMI_ACCOUNTS_OWNER_SUB?: string;
39
+ // Comma-separated allowlist of OAuth/OIDC subjects permitted to auto-provision
40
+ // a NON-owner (member) account. Empty/unset = member auto-provisioning is
41
+ // CLOSED (single-user default): once the owner exists, no new external subject
42
+ // can self-register a member just by completing the issuer's OAuth flow.
43
+ OIDC_ALLOWED_SUBS?: string;
44
+ TAKOS_URL?: string; // Optional Takos API base URL; not the OIDC issuer.
45
+ AUTH_MODE?: string;
46
+ ENCRYPTION_KEY?: string; // 32-byte hex key for encrypting sensitive data
47
+
48
+ // Per-deployment salt mixed into the SHA-256 of the session id before it is
49
+ // persisted as the session-row lookup key. The raw session id only ever
50
+ // lives in the client cookie; a read-only DB leak cannot be replayed without
51
+ // also recovering the raw id. Production deployments MUST set this to a
52
+ // high-entropy value (see hashSessionId in lib/crypto.ts). When
53
+ // YURUCOMMU_STRICT_READINESS is enabled and this is unset, the app warns.
54
+ YURUCOMMU_SESSION_HASH_SALT?: string;
55
+
56
+ // Shadow delivery probes (staging-only). Comma-separated hosts.
57
+ DELIVERY_SHADOW_PROBE_HOSTS?: string;
58
+ // 0.0-1.0 sampling rate for probes (default: 1.0)
59
+ DELIVERY_SHADOW_PROBE_SAMPLE_RATE?: string;
60
+ DELIVERY_QUEUE_NAME?: string;
61
+ DELIVERY_DLQ_NAME?: string;
62
+ YURUCOMMU_STRICT_READINESS?: string;
63
+ YURUCOMMU_ENABLE_LOCAL_SUBSTRATE_REMOTE_FETCHES?: string;
64
+ YURUCOMMU_ENABLE_LOCAL_DELIVERY_QUEUE?: string;
65
+ // Software version advertised in NodeInfo (software.version). The build /
66
+ // deploy pipeline should inject the real build version here; when unset the
67
+ // app falls back to the YURUCOMMU_VERSION default constant.
68
+ YURUCOMMU_SOFTWARE_VERSION?: string;
69
+
70
+ // CSRF allowed origins (comma-separated). APP_URL の origin に加えて
71
+ // 受け付ける追加 origin (= dev hostname (`https://yurucommu.test`) を
72
+ // production-equivalent な strict CSRF check 経由で踏むため)。 未設定なら
73
+ // 既存動作と同じ (= APP_URL 単一 origin のみ accept、 production 影響ゼロ)。
74
+ CSRF_ALLOWED_ORIGINS?: string;
75
+
76
+ // Declare the reverse-proxy type so the client-IP resolver trusts the right
77
+ // forwarding header (opt-in; a worker fronted directly by a client cannot
78
+ // spoof its own IP otherwise). Accepted values:
79
+ // "generic" — nginx / Caddy / Traefik: trust X-Forwarded-For (leftmost) /
80
+ // X-Real-IP, and IGNORE a client-settable CF-Connecting-IP.
81
+ // "cf" — a Cloudflare front (incl. cloudflared tunnel): trust
82
+ // CF-Connecting-IP.
83
+ // "true" / "1" — legacy alias: behaves like "generic" but keeps a
84
+ // CF-Connecting-IP fallback for back-compat.
85
+ // The genuine Cloudflare edge (unspoofable request.cf) always trusts
86
+ // CF-Connecting-IP with no opt-in.
87
+ TAKOS_TRUST_PROXY?: string;
88
+ }
89
+
90
+ /**
91
+ * Application Environment
92
+ *
93
+ * Uses the runtime-neutral `I*` contracts. The Cloudflare worker entry
94
+ * wraps the native `D1Database` / `R2Bucket` / `KVNamespace` / `Fetcher`
95
+ * bindings with the adapters in `runtime/cloudflare.ts` before handing
96
+ * the Env to Hono. The local runtime compatibility classes already
97
+ * implement these contracts directly.
98
+ *
99
+ * `DB_INSTANCE` is the drizzle wrapper that the app calls; it is built
100
+ * by each runtime entry point (Cloudflare `fetch`, local server, or
101
+ * runtime-specific wrappers).
102
+ */
103
+ export type Env = {
104
+ DB_INSTANCE: Database;
105
+ MEDIA?: IObjectStorage;
106
+ KV: IKeyValueStore;
107
+ ASSETS?: IStaticAssets;
108
+ DELIVERY_QUEUE?: Queue<DeliveryQueueMessageV1>;
109
+ DELIVERY_DLQ?: Queue<DeliveryDlqMessageV1>;
110
+ } & EnvVars;
111
+
112
+ export type Variables = {
113
+ actor: Actor | null;
114
+ db: Database;
115
+ oauthToken?: { sub: string; scope: string; client_id: string };
116
+ };
117
+
118
+ // Local actor (Person)
119
+ export interface Actor {
120
+ ap_id: string; // Primary key: https://domain/ap/users/username
121
+ type: string;
122
+ preferred_username: string;
123
+ name: string | null;
124
+ summary: string | null;
125
+ icon_url: string | null;
126
+ header_url: string | null;
127
+ inbox: string;
128
+ outbox: string;
129
+ followers_url: string;
130
+ following_url: string;
131
+ public_key_pem: string;
132
+ private_key_pem: string;
133
+ takos_user_id: string | null;
134
+ follower_count: number;
135
+ following_count: number;
136
+ post_count: number;
137
+ is_private: number;
138
+ role: "owner" | "moderator" | "member";
139
+ created_at: string;
140
+ }
141
+
142
+ // Re-export Hono types for route files
143
+ export type { Context } from "hono";
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Drizzle ORM Database Client for Yurucommu
3
+ *
4
+ * Multi-runtime support:
5
+ * - Cloudflare Workers (D1) via `drizzle-orm/d1`
6
+ * - Node.js / Bun (libsql) via `drizzle-orm/libsql`
7
+ *
8
+ * Both factories return distinct subclasses of `BaseSQLiteDatabase`, with
9
+ * different concrete result-row types (`D1Result` vs libsql's `ResultSet`).
10
+ * The query-builder surface this app uses (`select`, `insert`, `update`,
11
+ * `delete`, transactions) is inherited from the common base. The `Database`
12
+ * union below preserves both result-row types; consumers that need to
13
+ * inspect post-mutation metadata go through `affectedRowCount`.
14
+ */
15
+
16
+ import type { D1Database, D1Result } from "@cloudflare/workers-types";
17
+ import { drizzle as drizzleD1, type DrizzleD1Database } from "drizzle-orm/d1";
18
+ import type { LibSQLDatabase } from "drizzle-orm/libsql";
19
+ import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";
20
+ import type { ResultSet } from "@libsql/client";
21
+ import { isNull } from "drizzle-orm";
22
+ import * as schema from "./schema.ts";
23
+
24
+ export type Database = BaseSQLiteDatabase<
25
+ "async",
26
+ D1Result | ResultSet,
27
+ typeof schema
28
+ >;
29
+
30
+ /**
31
+ * Create a Drizzle client for Cloudflare D1
32
+ */
33
+ export function getDb(d1: D1Database): DrizzleD1Database<typeof schema> {
34
+ return drizzleD1(d1, { schema });
35
+ }
36
+
37
+ // Singleton for non-D1 runtimes
38
+ let sqliteDb: LibSQLDatabase<typeof schema> | null = null;
39
+
40
+ /**
41
+ * Create or get a Drizzle client for Node.js/Bun with SQLite file (libsql).
42
+ */
43
+ export async function getDbSQLite(databasePath: string): Promise<Database> {
44
+ if (sqliteDb) return sqliteDb;
45
+
46
+ const { createClient } = await import("@libsql/client");
47
+ const { drizzle } = await import("drizzle-orm/libsql");
48
+
49
+ const client = createClient({ url: `file:${databasePath}` });
50
+ // Foreign keys are explicitly turned OFF so the libsql engine matches
51
+ // Cloudflare D1, which ignores the FK constraints declared in the
52
+ // migrations. NOTE: libsql (unlike bun:sqlite / stock SQLite, which default
53
+ // OFF) defaults foreign_keys ON, so this must be set explicitly — simply
54
+ // not enabling it is not enough. Remote actors are stored in actor_cache
55
+ // (never in actors), yet objects.attributed_to / follows.* / likes.* /
56
+ // announces.* FK-reference actors(ap_id); enforcement would make every
57
+ // inbound federated activity from a remote actor violate the FK and fail to
58
+ // insert. Referential cleanup is handled at the app level by
59
+ // delete-cascade.ts, identically on D1.
60
+ await client.execute("PRAGMA foreign_keys = OFF");
61
+ sqliteDb = drizzle(client, { schema });
62
+ return sqliteDb;
63
+ }
64
+
65
+ /**
66
+ * Cross-runtime rows-affected accessor.
67
+ *
68
+ * Drizzle D1 returns `D1Result` with `meta.changes`; drizzle libsql returns
69
+ * `ResultSet` with `rowsAffected`. Use this helper to read affected-row
70
+ * counts portably after `update`/`delete`/`insert` builders.
71
+ */
72
+ export function affectedRowCount(result: unknown): number {
73
+ if (typeof result !== "object" || result === null) return 0;
74
+ const candidate = result as {
75
+ meta?: { changes?: number };
76
+ rowsAffected?: number;
77
+ };
78
+ if (typeof candidate.meta?.changes === "number") {
79
+ return candidate.meta.changes;
80
+ }
81
+ if (typeof candidate.rowsAffected === "number") return candidate.rowsAffected;
82
+ return 0;
83
+ }
84
+
85
+ /**
86
+ * Soft-delete helper: returns `isNull(table.deletedAt)` condition.
87
+ * Use in WHERE clauses for tables with soft-delete (actors, objects, communities).
88
+ */
89
+ export function notDeleted(
90
+ table:
91
+ typeof schema.actors | typeof schema.objects | typeof schema.communities,
92
+ ) {
93
+ return isNull(table.deletedAt);
94
+ }
95
+
96
+ export { nowIso } from "./schema.ts";
97
+ export * from "./schema.ts";
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Actor-related tables: actors, actorCache, instanceActor, sessions
3
+ */
4
+
5
+ import {
6
+ index,
7
+ integer,
8
+ sqliteTable,
9
+ text,
10
+ uniqueIndex,
11
+ } from "drizzle-orm/sqlite-core";
12
+ import { nowIso } from "./date-utils.ts";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // ACTORS
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export const actors = sqliteTable(
19
+ "actors",
20
+ {
21
+ apId: text("ap_id").primaryKey(),
22
+ type: text("type").notNull().default("Person"),
23
+ preferredUsername: text("preferred_username").notNull().unique(),
24
+ name: text("name"),
25
+ summary: text("summary"),
26
+ iconUrl: text("icon_url"),
27
+ headerUrl: text("header_url"),
28
+ inbox: text("inbox").notNull(),
29
+ outbox: text("outbox").notNull(),
30
+ followersUrl: text("followers_url").notNull(),
31
+ followingUrl: text("following_url").notNull(),
32
+ publicKeyPem: text("public_key_pem").notNull(),
33
+ privateKeyPem: text("private_key_pem").notNull(),
34
+ takosUserId: text("takos_user_id").unique(),
35
+ followerCount: integer("follower_count").notNull().default(0),
36
+ followingCount: integer("following_count").notNull().default(0),
37
+ postCount: integer("post_count").notNull().default(0),
38
+ isPrivate: integer("is_private").notNull().default(0),
39
+ role: text("role").notNull().default("member"),
40
+ // Structured profile metadata (Mastodon PropertyValue): JSON array of
41
+ // { name, value } rows rendered as `attachment` on the served actor doc.
42
+ fieldsJson: text("fields_json").notNull().default("[]"),
43
+ // Account-migration aliases: JSON array of AP IDs this account claims
44
+ // (alsoKnownAs). `movedTo` is the migration target once the account moves.
45
+ alsoKnownAsJson: text("also_known_as_json").notNull().default("[]"),
46
+ movedTo: text("moved_to"),
47
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
48
+ updatedAt: text("updated_at")
49
+ .notNull()
50
+ .$defaultFn(nowIso)
51
+ .$onUpdateFn(nowIso),
52
+ deletedAt: text("deleted_at"),
53
+ ownerActorApId: text("owner_actor_ap_id"),
54
+ },
55
+ (t) => [
56
+ index("actors_preferred_username_idx").on(t.preferredUsername),
57
+ index("actors_takos_user_id_idx").on(t.takosUserId),
58
+ index("actors_deleted_at_idx").on(t.deletedAt),
59
+ ],
60
+ );
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // ACTOR_CACHE
64
+ // ---------------------------------------------------------------------------
65
+
66
+ export const actorCache = sqliteTable("actor_cache", {
67
+ apId: text("ap_id").primaryKey(),
68
+ type: text("type").notNull().default("Person"),
69
+ preferredUsername: text("preferred_username"),
70
+ name: text("name"),
71
+ summary: text("summary"),
72
+ iconUrl: text("icon_url"),
73
+ inbox: text("inbox").notNull(),
74
+ outbox: text("outbox"),
75
+ followersUrl: text("followers_url"),
76
+ followingUrl: text("following_url"),
77
+ sharedInbox: text("shared_inbox"),
78
+ publicKeyId: text("public_key_id"),
79
+ publicKeyPem: text("public_key_pem"),
80
+ rawJson: text("raw_json").notNull(),
81
+ lastFetchedAt: text("last_fetched_at").notNull().$defaultFn(nowIso),
82
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
83
+ });
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // INSTANCE_ACTOR
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export const instanceActor = sqliteTable("instance_actor", {
90
+ apId: text("ap_id").primaryKey(),
91
+ preferredUsername: text("preferred_username").notNull(),
92
+ name: text("name"),
93
+ summary: text("summary"),
94
+ publicKeyPem: text("public_key_pem").notNull(),
95
+ privateKeyPem: text("private_key_pem").notNull(),
96
+ joinPolicy: text("join_policy").notNull().default("open"),
97
+ postingPolicy: text("posting_policy").notNull().default("members"),
98
+ visibility: text("visibility").notNull().default("public"),
99
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
100
+ updatedAt: text("updated_at")
101
+ .notNull()
102
+ .$defaultFn(nowIso)
103
+ .$onUpdateFn(nowIso),
104
+ });
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // SESSIONS
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export const sessions = sqliteTable(
111
+ "sessions",
112
+ {
113
+ id: text("id").primaryKey(),
114
+ memberId: text("member_id").notNull(),
115
+ accessToken: text("access_token").notNull().unique(),
116
+ refreshToken: text("refresh_token"),
117
+ expiresAt: text("expires_at").notNull(),
118
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
119
+ provider: text("provider"),
120
+ providerAccessToken: text("provider_access_token"),
121
+ providerRefreshToken: text("provider_refresh_token"),
122
+ providerTokenExpiresAt: text("provider_token_expires_at"),
123
+ },
124
+ (t) => [
125
+ index("sessions_member_idx").on(t.memberId),
126
+ index("sessions_provider_idx").on(t.provider),
127
+ index("sessions_expires_idx").on(t.expiresAt),
128
+ ],
129
+ );
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Community-related tables: communities, communityMembers, communityJoinRequests, communityInvites
3
+ */
4
+
5
+ import {
6
+ index,
7
+ integer,
8
+ primaryKey,
9
+ sqliteTable,
10
+ text,
11
+ } from "drizzle-orm/sqlite-core";
12
+ import { nowIso } from "./date-utils.ts";
13
+
14
+ // ---------------------------------------------------------------------------
15
+ // COMMUNITIES
16
+ // ---------------------------------------------------------------------------
17
+
18
+ export const communities = sqliteTable(
19
+ "communities",
20
+ {
21
+ apId: text("ap_id").primaryKey(),
22
+ type: text("type").notNull().default("Group"),
23
+ preferredUsername: text("preferred_username").notNull().unique(),
24
+ name: text("name").notNull(),
25
+ summary: text("summary"),
26
+ iconUrl: text("icon_url"),
27
+ inbox: text("inbox").notNull(),
28
+ outbox: text("outbox").notNull(),
29
+ followersUrl: text("followers_url").notNull(),
30
+ visibility: text("visibility").notNull().default("public"),
31
+ joinPolicy: text("join_policy").notNull().default("open"),
32
+ postPolicy: text("post_policy").notNull().default("members"),
33
+ publicKeyPem: text("public_key_pem").notNull(),
34
+ privateKeyPem: text("private_key_pem").notNull(),
35
+ createdBy: text("created_by").notNull(),
36
+ memberCount: integer("member_count").notNull().default(0),
37
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
38
+ lastMessageAt: text("last_message_at"),
39
+ deletedAt: text("deleted_at"),
40
+ },
41
+ (t) => [
42
+ index("communities_deleted_at_idx").on(t.deletedAt),
43
+ index("communities_visibility_idx").on(t.visibility),
44
+ ],
45
+ );
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // COMMUNITY_MEMBERS
49
+ // ---------------------------------------------------------------------------
50
+
51
+ export const communityMembers = sqliteTable(
52
+ "community_members",
53
+ {
54
+ communityApId: text("community_ap_id").notNull(),
55
+ actorApId: text("actor_ap_id").notNull(),
56
+ role: text("role").notNull().default("member"),
57
+ joinedAt: text("joined_at").notNull().$defaultFn(nowIso),
58
+ updatedAt: text("updated_at").$defaultFn(nowIso).$onUpdateFn(nowIso),
59
+ },
60
+ (t) => [
61
+ primaryKey({ columns: [t.communityApId, t.actorApId] }),
62
+ index("community_members_actor_idx").on(t.actorApId),
63
+ index("community_members_comm_role_joined_idx").on(
64
+ t.communityApId,
65
+ t.role,
66
+ t.joinedAt,
67
+ ),
68
+ ],
69
+ );
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // COMMUNITY_BANS — durable per-community bans so a kicked member cannot
73
+ // immediately re-join an open community (consulted by the local join route +
74
+ // the inbound Group Follow handler; cleared on explicit re-admission).
75
+ // ---------------------------------------------------------------------------
76
+
77
+ export const communityBans = sqliteTable(
78
+ "community_bans",
79
+ {
80
+ communityApId: text("community_ap_id").notNull(),
81
+ bannedApId: text("banned_ap_id").notNull(),
82
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
83
+ },
84
+ (t) => [
85
+ primaryKey({ columns: [t.communityApId, t.bannedApId] }),
86
+ index("community_bans_banned_idx").on(t.bannedApId),
87
+ ],
88
+ );
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // COMMUNITY_JOIN_REQUESTS
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export const communityJoinRequests = sqliteTable(
95
+ "community_join_requests",
96
+ {
97
+ communityApId: text("community_ap_id").notNull(),
98
+ actorApId: text("actor_ap_id").notNull(),
99
+ status: text("status").notNull().default("pending"),
100
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
101
+ processedAt: text("processed_at"),
102
+ },
103
+ (t) => [
104
+ primaryKey({ columns: [t.communityApId, t.actorApId] }),
105
+ index("community_join_requests_comm_status_idx").on(
106
+ t.communityApId,
107
+ t.status,
108
+ ),
109
+ index("community_join_requests_actor_idx").on(t.actorApId),
110
+ ],
111
+ );
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // COMMUNITY_INVITES
115
+ // ---------------------------------------------------------------------------
116
+
117
+ export const communityInvites = sqliteTable(
118
+ "community_invites",
119
+ {
120
+ id: text("id").primaryKey(),
121
+ communityApId: text("community_ap_id").notNull(),
122
+ invitedByApId: text("invited_by_ap_id").notNull(),
123
+ invitedApId: text("invited_ap_id"),
124
+ createdAt: text("created_at").notNull().$defaultFn(nowIso),
125
+ expiresAt: text("expires_at"),
126
+ usedAt: text("used_at"),
127
+ usedByApId: text("used_by_ap_id"),
128
+ },
129
+ (t) => [
130
+ index("community_invites_comm_idx").on(t.communityApId),
131
+ index("community_invites_invited_by_idx").on(t.invitedByApId),
132
+ ],
133
+ );
@@ -0,0 +1,17 @@
1
+ /** ISO timestamp with space separator for SQLite default values. */
2
+ export function nowIso(): string {
3
+ return new Date().toISOString().replace("T", " ").replace("Z", "");
4
+ }
5
+
6
+ /**
7
+ * Canonical ISO-8601 UTC timestamp (`2026-…T…Z`) — the SAME shape produced by
8
+ * every explicit `new Date().toISOString()` write and used as every range
9
+ * comparison operand. Use this (NOT the space-separated `nowIso`) as the default
10
+ * for any column that is range-COMPARED or SORTED lexically, e.g.
11
+ * `inbox.created_at`: under SQLite BINARY collation a space-format value sorts
12
+ * BELOW a same-instant `…Z` value (space 0x20 < `T` 0x54 at position 10), so a
13
+ * column written in BOTH formats mis-orders / mis-paginates rows.
14
+ */
15
+ export function nowIsoUtc(): string {
16
+ return new Date().toISOString();
17
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Schema barrel export
3
+ *
4
+ * Re-exports all tables and relations from domain-specific schema files.
5
+ */
6
+
7
+ export { nowIso } from "./date-utils.ts";
8
+
9
+ export * from "./actors.ts";
10
+ export * from "./posts.ts";
11
+ export * from "./social.ts";
12
+ export * from "./reports.ts";
13
+ export * from "./communities.ts";
14
+ export * from "./stories.ts";
15
+ export * from "./messaging.ts";
16
+ export * from "./mobile.ts";
17
+ export * from "./relations.ts";