@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
package/LICENSE ADDED
@@ -0,0 +1,16 @@
1
+ SPDX-License-Identifier: AGPL-3.0-only
2
+
3
+ GNU Affero General Public License v3.0 only
4
+
5
+ Copyright (c) 2026 Takos Contributors
6
+
7
+ This program is free software: you can redistribute it and/or modify it under
8
+ the terms of the GNU Affero General Public License as published by the Free
9
+ Software Foundation, version 3 of the License.
10
+
11
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY
12
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
13
+ PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
14
+
15
+ You should have received a copy of the GNU Affero General Public License along
16
+ with this program. If not, see <https://www.gnu.org/licenses/agpl-3.0.html>.
package/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # Yurucommu Core
2
+
3
+ Yurucommu Core is the shared **ActivityPub / API / DB / runtime engine library**
4
+ for the yurucommu family. It is not the installable product repo and it does not
5
+ own an OpenTofu Capsule.
6
+
7
+ The deployable products are separate repositories:
8
+
9
+ - `yurucommu` owns the feed / story / profile fullstack product, `yurucommu.com`,
10
+ its Worker artifact, and its OpenTofu Capsule.
11
+ - `yurumeet` owns the talk-first fullstack product. `yurume` is the short client
12
+ id / abbreviation used in discovery and push registration.
13
+
14
+ ## Features
15
+
16
+ - **ActivityPub federation** — follow, post, boost, like, and reply across
17
+ servers to link your communities and connections, with HTTP-signature
18
+ verification, SSRF-guarded fetches, and actor/domain blocklists.
19
+ - **Server API** — actor, post, story, DM, community, media, notification, and
20
+ mobile-push routes.
21
+ - **Client contract** — a typed SDK and discovery types for yurucommu,
22
+ Yurumeet, mobile, and alternate clients.
23
+ - **Search, notifications, recommendations** — discover people and content.
24
+ - **Auth** — password, Google / X OAuth, and Takosumi Accounts OIDC consumer.
25
+ - **Bilingual UI** — Japanese / English.
26
+
27
+ ## Deployment Boundary
28
+
29
+ Do not install this repo as a Capsule. Install `yurucommu` or `yurumeet`
30
+ instead. Those product repos bundle their own UI with this server engine, publish
31
+ their own Worker artifact, and expose their own plain OpenTofu module.
32
+
33
+ Client implementations should use `@takosjp/yurucommu-api`. Product Worker
34
+ artifacts use `@takosjp/yurucommu-core/server` to create the Hono backend and
35
+ `@takosjp/yurucommu-core/migrations` for D1 migration activation. They should
36
+ not import unpublished source paths from this checkout.
37
+
38
+ ## Develop
39
+
40
+ ```bash
41
+ cd yurucommu-core
42
+ bun install
43
+ bun run check # tsc --noEmit
44
+ bun test # bun:test suite
45
+ bun run lint # type check
46
+ bun run fmt # prettier
47
+ ```
48
+
49
+ The backend engine source lives in `src/backend` (Hono routes, ActivityPub
50
+ federation, delivery pipeline), the shared npm API package in `packages/api`,
51
+ and the database schema and migrations in `src/db/schema` and `migrations/`.
52
+
53
+ API package commands:
54
+
55
+ ```bash
56
+ bun run build:api # build @takosjp/yurucommu-api
57
+ bun run pack:api # npm pack --dry-run for the API package
58
+ ```
59
+
60
+ Yurumeet and mobile clients live in separate repositories. They should depend on
61
+ `@takosjp/yurucommu-api`, discover a server through Capsule outputs or
62
+ `/.well-known/social-server`, and deploy their own static/runtime artifact
63
+ through Takosumi, Cloudflare, or a self-host runtime. Local checkouts can be
64
+ placed under `clients/` for debugging, but that directory is intentionally
65
+ ignored so the server repo stays small.
66
+
67
+ When a client runs on a separate origin, the yurucommu-server must include that
68
+ origin in its CORS / CSRF allowlist.
69
+
70
+ ## Boundaries
71
+
72
+ Yurucommu implements its own ActivityPub federation, content distribution, and
73
+ user identity entirely at the app layer — it does not depend on Takos core
74
+ services or on a platform-layer federation mechanism. When installed through
75
+ Takosumi it is a normal Capsule a user can remove, and it is never absorbed into
76
+ Takos core. See [`AGENTS.md`](AGENTS.md) for the full product boundary.
77
+
78
+ ## Documentation
79
+
80
+ - [Deployment guide](https://yurucommu.com/help/deployment.html)
81
+ - [Getting started](https://yurucommu.com/help/getting-started.html)
82
+ - [Help site](https://yurucommu.com/help/)
@@ -0,0 +1,495 @@
1
+ -- Yurucommu Initial Schema (consolidated from Prisma schema)
2
+ -- Generated 2026-03-07
3
+
4
+ -- ============================================================
5
+ -- ACTORS (Local accounts - Person type)
6
+ -- ============================================================
7
+ CREATE TABLE IF NOT EXISTS actors (
8
+ ap_id TEXT PRIMARY KEY,
9
+ type TEXT NOT NULL DEFAULT 'Person',
10
+ preferred_username TEXT NOT NULL UNIQUE,
11
+ name TEXT,
12
+ summary TEXT,
13
+ icon_url TEXT,
14
+ header_url TEXT,
15
+ inbox TEXT NOT NULL,
16
+ outbox TEXT NOT NULL,
17
+ followers_url TEXT NOT NULL,
18
+ following_url TEXT NOT NULL,
19
+ public_key_pem TEXT NOT NULL,
20
+ private_key_pem TEXT NOT NULL,
21
+ takos_user_id TEXT UNIQUE,
22
+ follower_count INTEGER DEFAULT 0,
23
+ following_count INTEGER DEFAULT 0,
24
+ post_count INTEGER DEFAULT 0,
25
+ is_private INTEGER DEFAULT 0,
26
+ role TEXT DEFAULT 'member',
27
+ created_at TEXT DEFAULT (datetime('now')),
28
+ updated_at TEXT DEFAULT (datetime('now')),
29
+ deleted_at TEXT,
30
+ owner_actor_ap_id TEXT
31
+ );
32
+
33
+ CREATE INDEX IF NOT EXISTS idx_actors_preferred_username ON actors(preferred_username);
34
+ CREATE INDEX IF NOT EXISTS idx_actors_takos_user_id ON actors(takos_user_id);
35
+ CREATE INDEX IF NOT EXISTS idx_actors_deleted_at ON actors(deleted_at);
36
+
37
+ -- ============================================================
38
+ -- ACTOR_CACHE (Remote actors - cached from federation)
39
+ -- ============================================================
40
+ CREATE TABLE IF NOT EXISTS actor_cache (
41
+ ap_id TEXT PRIMARY KEY,
42
+ type TEXT NOT NULL DEFAULT 'Person',
43
+ preferred_username TEXT,
44
+ name TEXT,
45
+ summary TEXT,
46
+ icon_url TEXT,
47
+ inbox TEXT NOT NULL,
48
+ outbox TEXT,
49
+ followers_url TEXT,
50
+ following_url TEXT,
51
+ shared_inbox TEXT,
52
+ public_key_id TEXT,
53
+ public_key_pem TEXT,
54
+ raw_json TEXT NOT NULL,
55
+ last_fetched_at TEXT DEFAULT (datetime('now')),
56
+ created_at TEXT DEFAULT (datetime('now'))
57
+ );
58
+
59
+ -- ============================================================
60
+ -- COMMUNITIES (must be before objects due to FK reference)
61
+ -- ============================================================
62
+ CREATE TABLE IF NOT EXISTS communities (
63
+ ap_id TEXT PRIMARY KEY,
64
+ type TEXT NOT NULL DEFAULT 'Group',
65
+ preferred_username TEXT NOT NULL UNIQUE,
66
+ name TEXT NOT NULL,
67
+ summary TEXT,
68
+ icon_url TEXT,
69
+ inbox TEXT NOT NULL,
70
+ outbox TEXT NOT NULL,
71
+ followers_url TEXT NOT NULL,
72
+ visibility TEXT DEFAULT 'public',
73
+ join_policy TEXT DEFAULT 'open',
74
+ post_policy TEXT DEFAULT 'members',
75
+ public_key_pem TEXT NOT NULL,
76
+ private_key_pem TEXT NOT NULL,
77
+ created_by TEXT NOT NULL,
78
+ member_count INTEGER DEFAULT 0,
79
+ created_at TEXT DEFAULT (datetime('now')),
80
+ last_message_at TEXT,
81
+ deleted_at TEXT
82
+ );
83
+
84
+ CREATE INDEX IF NOT EXISTS idx_communities_deleted_at ON communities(deleted_at);
85
+ CREATE INDEX IF NOT EXISTS idx_communities_visibility ON communities(visibility);
86
+
87
+ -- ============================================================
88
+ -- OBJECTS (All AP objects - Note, Article, etc.)
89
+ -- ============================================================
90
+ CREATE TABLE IF NOT EXISTS objects (
91
+ ap_id TEXT PRIMARY KEY,
92
+ type TEXT NOT NULL DEFAULT 'Note',
93
+ attributed_to TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
94
+ content TEXT NOT NULL DEFAULT '',
95
+ summary TEXT,
96
+ attachments_json TEXT DEFAULT '[]',
97
+ in_reply_to TEXT,
98
+ conversation TEXT,
99
+ visibility TEXT DEFAULT 'public',
100
+ to_json TEXT DEFAULT '[]',
101
+ cc_json TEXT DEFAULT '[]',
102
+ audience_json TEXT DEFAULT '[]',
103
+ community_ap_id TEXT REFERENCES communities(ap_id) ON DELETE SET NULL,
104
+ end_time TEXT,
105
+ like_count INTEGER DEFAULT 0,
106
+ reply_count INTEGER DEFAULT 0,
107
+ announce_count INTEGER DEFAULT 0,
108
+ share_count INTEGER DEFAULT 0,
109
+ published TEXT DEFAULT (datetime('now')),
110
+ updated TEXT,
111
+ is_local INTEGER DEFAULT 1,
112
+ raw_json TEXT,
113
+ deleted_at TEXT
114
+ );
115
+
116
+ CREATE INDEX IF NOT EXISTS idx_objects_attributed_to ON objects(attributed_to);
117
+ CREATE INDEX IF NOT EXISTS idx_objects_in_reply_to ON objects(in_reply_to);
118
+ CREATE INDEX IF NOT EXISTS idx_objects_community_ap_id ON objects(community_ap_id);
119
+ CREATE INDEX IF NOT EXISTS idx_objects_published ON objects(published DESC);
120
+ CREATE INDEX IF NOT EXISTS idx_objects_visibility ON objects(visibility);
121
+ CREATE INDEX IF NOT EXISTS idx_objects_end_time ON objects(end_time);
122
+ CREATE INDEX IF NOT EXISTS idx_objects_deleted_at ON objects(deleted_at);
123
+ CREATE INDEX IF NOT EXISTS idx_objects_attributed_to_published ON objects(attributed_to, published DESC);
124
+ CREATE INDEX IF NOT EXISTS idx_objects_visibility_published ON objects(visibility, published DESC);
125
+ CREATE INDEX IF NOT EXISTS idx_objects_type_visibility_published ON objects(type, visibility, published DESC);
126
+ CREATE INDEX IF NOT EXISTS idx_objects_conversation ON objects(conversation);
127
+ CREATE INDEX IF NOT EXISTS idx_objects_community_published ON objects(community_ap_id, published DESC);
128
+ CREATE INDEX IF NOT EXISTS idx_objects_is_local ON objects(is_local);
129
+
130
+ -- ============================================================
131
+ -- FOLLOWS
132
+ -- ============================================================
133
+ CREATE TABLE IF NOT EXISTS follows (
134
+ follower_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
135
+ following_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
136
+ status TEXT DEFAULT 'pending',
137
+ activity_ap_id TEXT,
138
+ created_at TEXT DEFAULT (datetime('now')),
139
+ accepted_at TEXT,
140
+ PRIMARY KEY (follower_ap_id, following_ap_id)
141
+ );
142
+
143
+ CREATE INDEX IF NOT EXISTS idx_follows_follower_status ON follows(follower_ap_id, status);
144
+ CREATE INDEX IF NOT EXISTS idx_follows_following_status ON follows(following_ap_id, status);
145
+ CREATE INDEX IF NOT EXISTS idx_follows_following_created ON follows(following_ap_id, created_at DESC);
146
+ CREATE INDEX IF NOT EXISTS idx_follows_activity ON follows(activity_ap_id);
147
+
148
+ -- ============================================================
149
+ -- LIKES
150
+ -- ============================================================
151
+ CREATE TABLE IF NOT EXISTS likes (
152
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
153
+ object_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
154
+ activity_ap_id TEXT,
155
+ created_at TEXT DEFAULT (datetime('now')),
156
+ PRIMARY KEY (actor_ap_id, object_ap_id)
157
+ );
158
+
159
+ CREATE INDEX IF NOT EXISTS idx_likes_object ON likes(object_ap_id);
160
+ CREATE INDEX IF NOT EXISTS idx_likes_actor ON likes(actor_ap_id);
161
+ CREATE INDEX IF NOT EXISTS idx_likes_actor_object ON likes(actor_ap_id, object_ap_id);
162
+
163
+ -- ============================================================
164
+ -- ANNOUNCES (Reposts/Boosts)
165
+ -- ============================================================
166
+ CREATE TABLE IF NOT EXISTS announces (
167
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
168
+ object_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
169
+ activity_ap_id TEXT,
170
+ created_at TEXT DEFAULT (datetime('now')),
171
+ PRIMARY KEY (actor_ap_id, object_ap_id)
172
+ );
173
+
174
+ CREATE INDEX IF NOT EXISTS idx_announces_object ON announces(object_ap_id);
175
+ CREATE INDEX IF NOT EXISTS idx_announces_actor ON announces(actor_ap_id);
176
+ CREATE INDEX IF NOT EXISTS idx_announces_actor_object ON announces(actor_ap_id, object_ap_id);
177
+
178
+ -- ============================================================
179
+ -- BOOKMARKS
180
+ -- ============================================================
181
+ CREATE TABLE IF NOT EXISTS bookmarks (
182
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
183
+ object_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
184
+ created_at TEXT DEFAULT (datetime('now')),
185
+ PRIMARY KEY (actor_ap_id, object_ap_id)
186
+ );
187
+
188
+ CREATE INDEX IF NOT EXISTS idx_bookmarks_actor ON bookmarks(actor_ap_id);
189
+
190
+ -- ============================================================
191
+ -- BLOCKS
192
+ -- ============================================================
193
+ CREATE TABLE IF NOT EXISTS blocks (
194
+ blocker_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
195
+ blocked_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
196
+ created_at TEXT DEFAULT (datetime('now')),
197
+ updated_at TEXT DEFAULT (datetime('now')),
198
+ PRIMARY KEY (blocker_ap_id, blocked_ap_id)
199
+ );
200
+
201
+ CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_ap_id);
202
+ CREATE INDEX IF NOT EXISTS idx_blocks_blocked ON blocks(blocked_ap_id);
203
+
204
+ -- ============================================================
205
+ -- MUTES
206
+ -- ============================================================
207
+ CREATE TABLE IF NOT EXISTS mutes (
208
+ muter_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
209
+ muted_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
210
+ created_at TEXT DEFAULT (datetime('now')),
211
+ updated_at TEXT DEFAULT (datetime('now')),
212
+ PRIMARY KEY (muter_ap_id, muted_ap_id)
213
+ );
214
+
215
+ CREATE INDEX IF NOT EXISTS idx_mutes_muter ON mutes(muter_ap_id);
216
+ CREATE INDEX IF NOT EXISTS idx_mutes_muted ON mutes(muted_ap_id);
217
+
218
+ -- ============================================================
219
+ -- ACTIVITIES
220
+ -- ============================================================
221
+ CREATE TABLE IF NOT EXISTS activities (
222
+ ap_id TEXT PRIMARY KEY,
223
+ type TEXT NOT NULL,
224
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
225
+ object_ap_id TEXT REFERENCES objects(ap_id) ON DELETE SET NULL,
226
+ object_json TEXT,
227
+ target_ap_id TEXT,
228
+ raw_json TEXT NOT NULL,
229
+ direction TEXT,
230
+ processed INTEGER DEFAULT 0,
231
+ created_at TEXT DEFAULT (datetime('now'))
232
+ );
233
+
234
+ CREATE INDEX IF NOT EXISTS idx_activities_actor ON activities(actor_ap_id);
235
+ CREATE INDEX IF NOT EXISTS idx_activities_object ON activities(object_ap_id);
236
+ CREATE INDEX IF NOT EXISTS idx_activities_type ON activities(type);
237
+ CREATE INDEX IF NOT EXISTS idx_activities_type_created ON activities(type, created_at DESC);
238
+ CREATE INDEX IF NOT EXISTS idx_activities_direction_processed ON activities(direction, processed);
239
+ CREATE INDEX IF NOT EXISTS idx_activities_direction_processed_created ON activities(direction, processed, created_at);
240
+
241
+ -- ============================================================
242
+ -- DELIVERY_QUEUE
243
+ -- ============================================================
244
+ CREATE TABLE IF NOT EXISTS delivery_queue (
245
+ id TEXT PRIMARY KEY,
246
+ activity_ap_id TEXT NOT NULL,
247
+ inbox_url TEXT NOT NULL,
248
+ status TEXT DEFAULT 'pending',
249
+ attempts INTEGER DEFAULT 0,
250
+ last_attempt_at TEXT,
251
+ processing_started_at TEXT,
252
+ next_attempt_at TEXT DEFAULT (datetime('now')),
253
+ delivered_at TEXT,
254
+ error TEXT,
255
+ created_at TEXT DEFAULT (datetime('now'))
256
+ );
257
+
258
+ CREATE INDEX IF NOT EXISTS idx_delivery_queue_activity ON delivery_queue(activity_ap_id);
259
+ CREATE INDEX IF NOT EXISTS idx_delivery_queue_next ON delivery_queue(next_attempt_at);
260
+ CREATE INDEX IF NOT EXISTS idx_delivery_queue_status_next ON delivery_queue(status, next_attempt_at);
261
+
262
+ -- ============================================================
263
+ -- DELIVERY_CIRCUIT (Circuit breaker state per endpoint)
264
+ -- ============================================================
265
+ CREATE TABLE IF NOT EXISTS delivery_circuit (
266
+ endpoint TEXT PRIMARY KEY,
267
+ state TEXT DEFAULT 'closed',
268
+ consecutive_failures INTEGER DEFAULT 0,
269
+ recent_outcomes_json TEXT DEFAULT '[]',
270
+ open_until TEXT,
271
+ half_open_probe_attempts INTEGER DEFAULT 0,
272
+ half_open_probe_successes INTEGER DEFAULT 0,
273
+ created_at TEXT DEFAULT (datetime('now')),
274
+ updated_at TEXT DEFAULT (datetime('now'))
275
+ );
276
+
277
+ CREATE INDEX IF NOT EXISTS idx_delivery_circuit_state ON delivery_circuit(state, updated_at);
278
+
279
+ -- ============================================================
280
+ -- COMMUNITY_MEMBERS
281
+ -- ============================================================
282
+ CREATE TABLE IF NOT EXISTS community_members (
283
+ community_ap_id TEXT NOT NULL REFERENCES communities(ap_id) ON DELETE CASCADE,
284
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
285
+ role TEXT DEFAULT 'member',
286
+ joined_at TEXT DEFAULT (datetime('now')),
287
+ updated_at TEXT DEFAULT (datetime('now')),
288
+ PRIMARY KEY (community_ap_id, actor_ap_id)
289
+ );
290
+
291
+ CREATE INDEX IF NOT EXISTS idx_community_members_actor ON community_members(actor_ap_id);
292
+ CREATE INDEX IF NOT EXISTS idx_community_members_role ON community_members(community_ap_id, role, joined_at);
293
+
294
+ -- ============================================================
295
+ -- COMMUNITY_JOIN_REQUESTS
296
+ -- ============================================================
297
+ CREATE TABLE IF NOT EXISTS community_join_requests (
298
+ community_ap_id TEXT NOT NULL REFERENCES communities(ap_id) ON DELETE CASCADE,
299
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
300
+ status TEXT DEFAULT 'pending',
301
+ created_at TEXT DEFAULT (datetime('now')),
302
+ processed_at TEXT,
303
+ PRIMARY KEY (community_ap_id, actor_ap_id)
304
+ );
305
+
306
+ CREATE INDEX IF NOT EXISTS idx_community_join_requests_status ON community_join_requests(community_ap_id, status);
307
+ CREATE INDEX IF NOT EXISTS idx_community_join_requests_actor ON community_join_requests(actor_ap_id);
308
+
309
+ -- ============================================================
310
+ -- COMMUNITY_INVITES
311
+ -- ============================================================
312
+ CREATE TABLE IF NOT EXISTS community_invites (
313
+ id TEXT PRIMARY KEY,
314
+ community_ap_id TEXT NOT NULL REFERENCES communities(ap_id) ON DELETE CASCADE,
315
+ invited_by_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
316
+ invited_ap_id TEXT,
317
+ created_at TEXT DEFAULT (datetime('now')),
318
+ expires_at TEXT,
319
+ used_at TEXT,
320
+ used_by_ap_id TEXT
321
+ );
322
+
323
+ CREATE INDEX IF NOT EXISTS idx_community_invites_community ON community_invites(community_ap_id);
324
+ CREATE INDEX IF NOT EXISTS idx_community_invites_invited_by ON community_invites(invited_by_ap_id);
325
+
326
+ -- ============================================================
327
+ -- OBJECT_RECIPIENTS
328
+ -- ============================================================
329
+ CREATE TABLE IF NOT EXISTS object_recipients (
330
+ object_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
331
+ recipient_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
332
+ type TEXT NOT NULL,
333
+ created_at TEXT DEFAULT (datetime('now')),
334
+ PRIMARY KEY (object_ap_id, recipient_ap_id)
335
+ );
336
+
337
+ CREATE INDEX IF NOT EXISTS idx_object_recipients_recipient ON object_recipients(recipient_ap_id, created_at DESC);
338
+
339
+ -- ============================================================
340
+ -- INBOX
341
+ -- ============================================================
342
+ CREATE TABLE IF NOT EXISTS inbox (
343
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
344
+ activity_ap_id TEXT NOT NULL REFERENCES activities(ap_id) ON DELETE CASCADE,
345
+ read INTEGER DEFAULT 0,
346
+ created_at TEXT DEFAULT (datetime('now')),
347
+ PRIMARY KEY (actor_ap_id, activity_ap_id)
348
+ );
349
+
350
+ CREATE INDEX IF NOT EXISTS idx_inbox_actor_read ON inbox(actor_ap_id, read, created_at DESC);
351
+ CREATE INDEX IF NOT EXISTS idx_inbox_activity ON inbox(activity_ap_id);
352
+
353
+ -- ============================================================
354
+ -- SESSIONS
355
+ -- ============================================================
356
+ CREATE TABLE IF NOT EXISTS sessions (
357
+ id TEXT PRIMARY KEY,
358
+ member_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
359
+ access_token TEXT NOT NULL UNIQUE,
360
+ refresh_token TEXT,
361
+ expires_at TEXT NOT NULL,
362
+ created_at TEXT DEFAULT (datetime('now')),
363
+ provider TEXT,
364
+ provider_access_token TEXT,
365
+ provider_refresh_token TEXT,
366
+ provider_token_expires_at TEXT
367
+ );
368
+
369
+ CREATE INDEX IF NOT EXISTS idx_sessions_member ON sessions(member_id);
370
+ CREATE INDEX IF NOT EXISTS idx_sessions_provider ON sessions(provider);
371
+ CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
372
+
373
+ -- ============================================================
374
+ -- STORY_VIEWS
375
+ -- ============================================================
376
+ CREATE TABLE IF NOT EXISTS story_views (
377
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
378
+ story_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
379
+ viewed_at TEXT DEFAULT (datetime('now')),
380
+ PRIMARY KEY (actor_ap_id, story_ap_id)
381
+ );
382
+
383
+ CREATE INDEX IF NOT EXISTS idx_story_views_actor ON story_views(actor_ap_id);
384
+ CREATE INDEX IF NOT EXISTS idx_story_views_story ON story_views(story_ap_id);
385
+
386
+ -- ============================================================
387
+ -- STORY_VOTES
388
+ -- ============================================================
389
+ CREATE TABLE IF NOT EXISTS story_votes (
390
+ id TEXT PRIMARY KEY,
391
+ story_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
392
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
393
+ option_index INTEGER NOT NULL,
394
+ created_at TEXT DEFAULT (datetime('now')),
395
+ UNIQUE(story_ap_id, actor_ap_id)
396
+ );
397
+
398
+ CREATE INDEX IF NOT EXISTS idx_story_votes_story ON story_votes(story_ap_id);
399
+ CREATE INDEX IF NOT EXISTS idx_story_votes_actor ON story_votes(actor_ap_id);
400
+
401
+ -- ============================================================
402
+ -- STORY_SHARES
403
+ -- ============================================================
404
+ CREATE TABLE IF NOT EXISTS story_shares (
405
+ id TEXT PRIMARY KEY,
406
+ story_ap_id TEXT NOT NULL REFERENCES objects(ap_id) ON DELETE CASCADE,
407
+ actor_ap_id TEXT NOT NULL REFERENCES actors(ap_id) ON DELETE CASCADE,
408
+ shared_at TEXT DEFAULT (datetime('now')),
409
+ UNIQUE(story_ap_id, actor_ap_id)
410
+ );
411
+
412
+ CREATE INDEX IF NOT EXISTS idx_story_shares_story ON story_shares(story_ap_id);
413
+ CREATE INDEX IF NOT EXISTS idx_story_shares_actor ON story_shares(actor_ap_id);
414
+
415
+ -- ============================================================
416
+ -- NOTIFICATION_ARCHIVED
417
+ -- ============================================================
418
+ CREATE TABLE IF NOT EXISTS notification_archived (
419
+ actor_ap_id TEXT NOT NULL,
420
+ activity_ap_id TEXT NOT NULL,
421
+ archived_at TEXT DEFAULT (datetime('now')),
422
+ PRIMARY KEY (actor_ap_id, activity_ap_id)
423
+ );
424
+
425
+ CREATE INDEX IF NOT EXISTS idx_notification_archived_actor ON notification_archived(actor_ap_id);
426
+ CREATE INDEX IF NOT EXISTS idx_notification_archived_actor_at ON notification_archived(actor_ap_id, archived_at);
427
+
428
+ -- ============================================================
429
+ -- INSTANCE_ACTOR
430
+ -- ============================================================
431
+ CREATE TABLE IF NOT EXISTS instance_actor (
432
+ ap_id TEXT PRIMARY KEY,
433
+ preferred_username TEXT NOT NULL,
434
+ name TEXT,
435
+ summary TEXT,
436
+ public_key_pem TEXT NOT NULL,
437
+ private_key_pem TEXT NOT NULL,
438
+ join_policy TEXT DEFAULT 'open',
439
+ posting_policy TEXT DEFAULT 'members',
440
+ visibility TEXT DEFAULT 'public',
441
+ created_at TEXT DEFAULT (datetime('now')),
442
+ updated_at TEXT DEFAULT (datetime('now'))
443
+ );
444
+
445
+ -- ============================================================
446
+ -- DM_TYPING
447
+ -- ============================================================
448
+ CREATE TABLE IF NOT EXISTS dm_typing (
449
+ actor_ap_id TEXT NOT NULL,
450
+ recipient_ap_id TEXT NOT NULL,
451
+ last_typed_at TEXT NOT NULL,
452
+ PRIMARY KEY (actor_ap_id, recipient_ap_id)
453
+ );
454
+
455
+ CREATE INDEX IF NOT EXISTS idx_dm_typing_recipient ON dm_typing(recipient_ap_id, last_typed_at DESC);
456
+
457
+ -- ============================================================
458
+ -- DM_READ_STATUS
459
+ -- ============================================================
460
+ CREATE TABLE IF NOT EXISTS dm_read_status (
461
+ actor_ap_id TEXT NOT NULL,
462
+ conversation_id TEXT NOT NULL,
463
+ last_read_at TEXT DEFAULT (datetime('now')),
464
+ PRIMARY KEY (actor_ap_id, conversation_id)
465
+ );
466
+
467
+ CREATE INDEX IF NOT EXISTS idx_dm_read_status_actor ON dm_read_status(actor_ap_id);
468
+ CREATE INDEX IF NOT EXISTS idx_dm_read_status_actor_at ON dm_read_status(actor_ap_id, last_read_at DESC);
469
+
470
+ -- ============================================================
471
+ -- DM_ARCHIVED_CONVERSATIONS
472
+ -- ============================================================
473
+ CREATE TABLE IF NOT EXISTS dm_archived_conversations (
474
+ actor_ap_id TEXT NOT NULL,
475
+ conversation_id TEXT NOT NULL,
476
+ archived_at TEXT DEFAULT (datetime('now')),
477
+ PRIMARY KEY (actor_ap_id, conversation_id)
478
+ );
479
+
480
+ CREATE INDEX IF NOT EXISTS idx_dm_archived_actor ON dm_archived_conversations(actor_ap_id);
481
+
482
+ -- ============================================================
483
+ -- MEDIA_UPLOADS
484
+ -- ============================================================
485
+ CREATE TABLE IF NOT EXISTS media_uploads (
486
+ id TEXT PRIMARY KEY,
487
+ r2_key TEXT NOT NULL UNIQUE,
488
+ uploader_ap_id TEXT NOT NULL,
489
+ content_type TEXT NOT NULL,
490
+ size INTEGER NOT NULL,
491
+ created_at TEXT DEFAULT (datetime('now'))
492
+ );
493
+
494
+ CREATE INDEX IF NOT EXISTS idx_media_uploads_uploader ON media_uploads(uploader_ap_id);
495
+ CREATE INDEX IF NOT EXISTS idx_media_uploads_r2_key ON media_uploads(r2_key);
@@ -0,0 +1,92 @@
1
+ -- Social edge tables store ActivityPub actor IDs. Either side can be a
2
+ -- remote actor cached in actor_cache, so physical FKs to local actors block
3
+ -- federation under SQLite/D1 foreign-key enforcement.
4
+
5
+ CREATE TABLE IF NOT EXISTS follows_new (
6
+ follower_ap_id TEXT NOT NULL,
7
+ following_ap_id TEXT NOT NULL,
8
+ status TEXT NOT NULL DEFAULT 'pending',
9
+ activity_ap_id TEXT,
10
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
11
+ accepted_at TEXT,
12
+ PRIMARY KEY (follower_ap_id, following_ap_id)
13
+ );
14
+
15
+ INSERT OR IGNORE INTO follows_new (
16
+ follower_ap_id,
17
+ following_ap_id,
18
+ status,
19
+ activity_ap_id,
20
+ created_at,
21
+ accepted_at
22
+ )
23
+ SELECT
24
+ follower_ap_id,
25
+ following_ap_id,
26
+ COALESCE(status, 'pending'),
27
+ activity_ap_id,
28
+ COALESCE(created_at, datetime('now')),
29
+ accepted_at
30
+ FROM follows;
31
+
32
+ DROP TABLE follows;
33
+ ALTER TABLE follows_new RENAME TO follows;
34
+
35
+ CREATE INDEX IF NOT EXISTS idx_follows_follower_status ON follows(follower_ap_id, status);
36
+ CREATE INDEX IF NOT EXISTS idx_follows_following_status ON follows(following_ap_id, status);
37
+ CREATE INDEX IF NOT EXISTS idx_follows_following_created ON follows(following_ap_id, created_at DESC);
38
+ CREATE INDEX IF NOT EXISTS idx_follows_activity ON follows(activity_ap_id);
39
+
40
+ CREATE TABLE IF NOT EXISTS blocks_new (
41
+ blocker_ap_id TEXT NOT NULL,
42
+ blocked_ap_id TEXT NOT NULL,
43
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
44
+ updated_at TEXT DEFAULT (datetime('now')),
45
+ PRIMARY KEY (blocker_ap_id, blocked_ap_id)
46
+ );
47
+
48
+ INSERT OR IGNORE INTO blocks_new (
49
+ blocker_ap_id,
50
+ blocked_ap_id,
51
+ created_at,
52
+ updated_at
53
+ )
54
+ SELECT
55
+ blocker_ap_id,
56
+ blocked_ap_id,
57
+ COALESCE(created_at, datetime('now')),
58
+ updated_at
59
+ FROM blocks;
60
+
61
+ DROP TABLE blocks;
62
+ ALTER TABLE blocks_new RENAME TO blocks;
63
+
64
+ CREATE INDEX IF NOT EXISTS idx_blocks_blocker ON blocks(blocker_ap_id);
65
+ CREATE INDEX IF NOT EXISTS idx_blocks_blocked ON blocks(blocked_ap_id);
66
+
67
+ CREATE TABLE IF NOT EXISTS mutes_new (
68
+ muter_ap_id TEXT NOT NULL,
69
+ muted_ap_id TEXT NOT NULL,
70
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
71
+ updated_at TEXT DEFAULT (datetime('now')),
72
+ PRIMARY KEY (muter_ap_id, muted_ap_id)
73
+ );
74
+
75
+ INSERT OR IGNORE INTO mutes_new (
76
+ muter_ap_id,
77
+ muted_ap_id,
78
+ created_at,
79
+ updated_at
80
+ )
81
+ SELECT
82
+ muter_ap_id,
83
+ muted_ap_id,
84
+ COALESCE(created_at, datetime('now')),
85
+ updated_at
86
+ FROM mutes;
87
+
88
+ DROP TABLE mutes;
89
+ ALTER TABLE mutes_new RENAME TO mutes;
90
+
91
+ CREATE INDEX IF NOT EXISTS idx_mutes_muter ON mutes(muter_ap_id);
92
+ CREATE INDEX IF NOT EXISTS idx_mutes_muted ON mutes(muted_ap_id);