@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.
- package/LICENSE +16 -0
- package/README.md +82 -0
- package/migrations/0001_init.sql +495 -0
- package/migrations/0002_social_remote_actor_edges.sql +92 -0
- package/migrations/0003_activity_remote_object_edges.sql +68 -0
- package/migrations/0004_blocklist.sql +26 -0
- package/migrations/0005_story_community_scope.sql +13 -0
- package/migrations/0006_dm_community_read_status.sql +19 -0
- package/migrations/0007_moderation_reports.sql +22 -0
- package/migrations/0008_actor_fields_aka.sql +18 -0
- package/migrations/0009_object_tags.sql +13 -0
- package/migrations/0010_object_recipients_drop_actor_fk.sql +34 -0
- package/migrations/0011_drop_remote_actor_fks.sql +205 -0
- package/migrations/0012_objects_content_fts.sql +39 -0
- package/migrations/0013_efficiency_indexes.sql +13 -0
- package/migrations/0014_inbox_actor_created_idx.sql +15 -0
- package/migrations/0015_community_bans.sql +16 -0
- package/migrations/0016_namespace_takos_oidc_subject.sql +19 -0
- package/migrations/0017_mobile_push_registrations.sql +22 -0
- package/migrations/README.md +122 -0
- package/package.json +75 -0
- package/packages/api/LICENSE +16 -0
- package/packages/api/package.json +30 -0
- package/packages/api/src/index.ts +4 -0
- package/packages/api/src/lib/api/account.ts +20 -0
- package/packages/api/src/lib/api/actors.ts +149 -0
- package/packages/api/src/lib/api/auth.ts +46 -0
- package/packages/api/src/lib/api/communities.ts +329 -0
- package/packages/api/src/lib/api/dm.test.ts +67 -0
- package/packages/api/src/lib/api/dm.ts +236 -0
- package/packages/api/src/lib/api/fetch.ts +111 -0
- package/packages/api/src/lib/api/follow.ts +30 -0
- package/packages/api/src/lib/api/media.ts +100 -0
- package/packages/api/src/lib/api/moderation.ts +98 -0
- package/packages/api/src/lib/api/normalize.ts +71 -0
- package/packages/api/src/lib/api/notifications.test.ts +63 -0
- package/packages/api/src/lib/api/notifications.ts +61 -0
- package/packages/api/src/lib/api/posts.test.ts +110 -0
- package/packages/api/src/lib/api/posts.ts +181 -0
- package/packages/api/src/lib/api/recommendations.ts +22 -0
- package/packages/api/src/lib/api/search.ts +88 -0
- package/packages/api/src/lib/api/stories.ts +80 -0
- package/packages/api/src/lib/api.ts +15 -0
- package/packages/api/src/lib/fetch-with-timeout.ts +42 -0
- package/packages/api/src/lib/transport.ts +40 -0
- package/packages/api/src/social-server.ts +47 -0
- package/packages/api/src/types/index.ts +185 -0
- package/scripts/apply-takosumi-migrations.ts +621 -0
- package/src/backend/federation-helpers.ts +36 -0
- package/src/backend/index.ts +872 -0
- package/src/backend/lib/account-migration.ts +106 -0
- package/src/backend/lib/activitypub-actor-cache.ts +238 -0
- package/src/backend/lib/activitypub-helpers.ts +131 -0
- package/src/backend/lib/activitypub-validators.ts +323 -0
- package/src/backend/lib/ap-context.ts +16 -0
- package/src/backend/lib/ap-ids.ts +101 -0
- package/src/backend/lib/ap-response.ts +30 -0
- package/src/backend/lib/ap-signing.ts +87 -0
- package/src/backend/lib/ap-verify.ts +670 -0
- package/src/backend/lib/auth-lockout.ts +230 -0
- package/src/backend/lib/backend-paths.ts +34 -0
- package/src/backend/lib/base64.ts +30 -0
- package/src/backend/lib/blocklist-purge.ts +109 -0
- package/src/backend/lib/blocklist.ts +279 -0
- package/src/backend/lib/chunk.ts +33 -0
- package/src/backend/lib/client-ip.ts +169 -0
- package/src/backend/lib/community-visibility.ts +230 -0
- package/src/backend/lib/crypto.ts +424 -0
- package/src/backend/lib/delivery/circuit.ts +265 -0
- package/src/backend/lib/delivery/metrics.ts +30 -0
- package/src/backend/lib/delivery/planner.ts +190 -0
- package/src/backend/lib/delivery/queue-batching.ts +626 -0
- package/src/backend/lib/delivery/queue-delivery.ts +641 -0
- package/src/backend/lib/delivery/queue.ts +576 -0
- package/src/backend/lib/delivery/transformers.ts +56 -0
- package/src/backend/lib/delivery/types.ts +139 -0
- package/src/backend/lib/errors.ts +114 -0
- package/src/backend/lib/federation-fetch.ts +296 -0
- package/src/backend/lib/feed-cursor.ts +57 -0
- package/src/backend/lib/feed-exclude.ts +48 -0
- package/src/backend/lib/hex.ts +8 -0
- package/src/backend/lib/log-mask.ts +213 -0
- package/src/backend/lib/logger.ts +285 -0
- package/src/backend/lib/mobile-contract.ts +137 -0
- package/src/backend/lib/oauth-providers.ts +324 -0
- package/src/backend/lib/oauth-utils.ts +148 -0
- package/src/backend/lib/oidc-id-token.ts +151 -0
- package/src/backend/lib/parse-helpers.ts +31 -0
- package/src/backend/lib/post-visibility.ts +190 -0
- package/src/backend/lib/session-actor.ts +61 -0
- package/src/backend/lib/ssrf.ts +428 -0
- package/src/backend/lib/strip-image-metadata.ts +191 -0
- package/src/backend/middleware/bearer-auth.ts +70 -0
- package/src/backend/middleware/body-limit.ts +212 -0
- package/src/backend/middleware/cache.ts +429 -0
- package/src/backend/middleware/csrf.ts +130 -0
- package/src/backend/middleware/error-handler.ts +77 -0
- package/src/backend/middleware/rate-limit.ts +308 -0
- package/src/backend/public.ts +21 -0
- package/src/backend/routes/account-teardown.ts +430 -0
- package/src/backend/routes/activitypub/handlers/actor-inbox-handlers.ts +354 -0
- package/src/backend/routes/activitypub/handlers/inbound-timestamp.ts +29 -0
- package/src/backend/routes/activitypub/handlers/inbox-content-handlers.ts +1634 -0
- package/src/backend/routes/activitypub/handlers/inbox-follow-handlers.ts +547 -0
- package/src/backend/routes/activitypub/handlers/inbox-interaction-handlers.ts +497 -0
- package/src/backend/routes/activitypub/handlers/inbox-shared-helpers.ts +262 -0
- package/src/backend/routes/activitypub/handlers/user-inbox-handlers.ts +35 -0
- package/src/backend/routes/activitypub/inbox-types.ts +74 -0
- package/src/backend/routes/activitypub/inbox.ts +1191 -0
- package/src/backend/routes/activitypub/outbox.ts +0 -0
- package/src/backend/routes/activitypub/query-helpers.ts +227 -0
- package/src/backend/routes/activitypub.ts +616 -0
- package/src/backend/routes/actors-helpers.ts +487 -0
- package/src/backend/routes/actors.ts +1311 -0
- package/src/backend/routes/apps.ts +313 -0
- package/src/backend/routes/auth-helpers.ts +566 -0
- package/src/backend/routes/auth.ts +615 -0
- package/src/backend/routes/communities/membership-invites.ts +208 -0
- package/src/backend/routes/communities/membership-join.ts +335 -0
- package/src/backend/routes/communities/membership-members.ts +539 -0
- package/src/backend/routes/communities/membership-requests.ts +296 -0
- package/src/backend/routes/communities/membership-shared.ts +364 -0
- package/src/backend/routes/communities/messages.ts +479 -0
- package/src/backend/routes/communities/routes.ts +624 -0
- package/src/backend/routes/communities.ts +21 -0
- package/src/backend/routes/dm/contacts.ts +525 -0
- package/src/backend/routes/dm/conversations-helpers.ts +197 -0
- package/src/backend/routes/dm/conversations.ts +25 -0
- package/src/backend/routes/dm/messages.ts +658 -0
- package/src/backend/routes/dm/query-helpers.ts +85 -0
- package/src/backend/routes/dm/read-archive.ts +228 -0
- package/src/backend/routes/dm/requests.ts +222 -0
- package/src/backend/routes/dm/typing.ts +81 -0
- package/src/backend/routes/dm.ts +15 -0
- package/src/backend/routes/follow-helpers.ts +370 -0
- package/src/backend/routes/follow.ts +588 -0
- package/src/backend/routes/media.ts +692 -0
- package/src/backend/routes/mobile.ts +159 -0
- package/src/backend/routes/moderation.ts +373 -0
- package/src/backend/routes/notifications.ts +757 -0
- package/src/backend/routes/posts/delete-cascade.ts +330 -0
- package/src/backend/routes/posts/interactions.ts +795 -0
- package/src/backend/routes/posts/post-helpers.ts +847 -0
- package/src/backend/routes/posts/queries.ts +537 -0
- package/src/backend/routes/posts/routes.ts +865 -0
- package/src/backend/routes/posts/transformers.ts +161 -0
- package/src/backend/routes/posts.ts +17 -0
- package/src/backend/routes/recommendations.ts +88 -0
- package/src/backend/routes/search.ts +730 -0
- package/src/backend/routes/stories/interactions.ts +576 -0
- package/src/backend/routes/stories/query-helpers.ts +482 -0
- package/src/backend/routes/stories/routes.ts +906 -0
- package/src/backend/routes/stories.ts +13 -0
- package/src/backend/routes/takos-tools/dm.ts +249 -0
- package/src/backend/routes/takos-tools/follows.ts +225 -0
- package/src/backend/routes/takos-tools/posts.ts +292 -0
- package/src/backend/routes/takos-tools/search.ts +228 -0
- package/src/backend/routes/takos-tools/timeline.ts +132 -0
- package/src/backend/routes/takos-tools/types.ts +10 -0
- package/src/backend/routes/takos-tools-response.ts +178 -0
- package/src/backend/routes/takos-tools.ts +153 -0
- package/src/backend/routes/timeline.ts +755 -0
- package/src/backend/runtime/bun.ts +620 -0
- package/src/backend/runtime/cloudflare.ts +202 -0
- package/src/backend/runtime/compat-bun/types.ts +44 -0
- package/src/backend/runtime/memory-kv.ts +104 -0
- package/src/backend/runtime/shared.ts +142 -0
- package/src/backend/runtime/types.ts +205 -0
- package/src/backend/server.ts +636 -0
- package/src/backend/types.ts +143 -0
- package/src/db/index.ts +97 -0
- package/src/db/schema/actors.ts +129 -0
- package/src/db/schema/communities.ts +133 -0
- package/src/db/schema/date-utils.ts +17 -0
- package/src/db/schema/index.ts +17 -0
- package/src/db/schema/messaging.ts +241 -0
- package/src/db/schema/mobile.ts +37 -0
- package/src/db/schema/posts.ts +150 -0
- package/src/db/schema/relations.ts +266 -0
- package/src/db/schema/reports.ts +33 -0
- package/src/db/schema/social.ts +106 -0
- package/src/db/schema/stories.ts +70 -0
- package/src/db/schema.ts +15 -0
- package/src/plugin/public.ts +7 -0
- 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";
|
package/src/db/index.ts
ADDED
|
@@ -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";
|