sipher/src/lib/plugins/social/server/helpers/social.ts
Nixyan 66ebebd105 refactor: modularize plugins with federation and encryption infrastructure
Major changes:
- Restructure plugin architecture: moved federation logic into a dedicated `federation` plugin with Better Auth integration, defining schemas for server registry, key rotation, and blacklist management
- Extract encryption layer: new `oven` plugin handles end-to-end encryption (E2EE) with OLM client/server implementations
- Reorganize social features: consolidated social endpoints (posts, follows, blocks, mutes) and removed legacy plugin patterns in favor of unified plugin structure
- Decentralized key management: refactored `keytools` and `keygen` to support federation key rotation with challenge tokens and health checks

Infrastructure updates:
- Upgrade dependencies: bump Better Auth to 1.6.9, React to 19.2.5, Next.js to 16.2.3, Tailwind to 4.2.4
- Add cryptographic libraries: @scure/bip39, @signalapp/libsignal-client, @matrix-org/matrix-sdk-crypto-wasm for enhanced federation security
- Add utilities: base58-js, uuid for federation identifier handling
- Update database schema with new federation tables (serverRegistry, rotateChallengeTokens, blacklistedServers)

Minor updates: test suite alignment, storage client cleanup, PostFederationSchema refinements

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-05 11:40:14 -03:00

270 lines
No EOL
5.7 KiB
TypeScript

import { BetterAuthPluginDBSchema } from "better-auth";
import { z } from "zod";
const postContentBlockSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("text"),
value: z.string().min(1, "Text content cannot be empty"),
}),
z.object({
type: z.literal("image"),
url: z.url("Image must be a valid URL"),
index: z.number().min(0, "Index must be a positive number"),
size: z.number().min(0, "Size must be a positive number"),
}),
z.object({
type: z.literal("video"),
url: z.url("Video must be a valid URL"),
size: z.number().min(0, "Size must be a positive number"),
index: z.number().min(0, "Index must be a positive number"),
}),
z.object({
type: z.literal("audio"),
url: z.url("Audio must be a valid URL"),
size: z.number().min(0, "Size must be a positive number"),
}),
z.object({
type: z.literal("link"),
url: z.url("Link must be a valid URL"),
}),
], { error: 'Block "type" must be one of: text, image, video, audio, link' });
export const postContentSchema = z
.array(postContentBlockSchema, { error: "Post content must be an array of blocks" })
.min(1, "Post must contain at least one content block");
export default {
posts: {
fields: {
content: {
type: "json",
required: true,
index: false,
transform: {
output: (value) => {
let parsed: unknown;
try {
parsed = typeof value === "string" ? JSON.parse(value) : value;
} catch {
throw new Error("Post content is not valid JSON");
}
const validated = postContentSchema.safeParse(parsed);
if (!validated.success) {
const issues = validated.error.issues
.map((i) => `[${i.path.join(".")}] ${i.message}`)
.join("; ");
throw new Error(`Invalid post content: ${issues}`);
}
return validated.data;
}
}
},
authorId: {
type: "string",
required: false,
index: false,
references: {
model: "user",
field: "id"
}
},
federatedAuthorId: {
type: "string",
required: false,
index: false,
},
published: {
type: "date",
required: true,
index: false,
},
// "isLocal" will be used to determine if the post should only exist
// on the local server or if it should be propagated to other servers
isLocal: {
type: "boolean",
required: true,
index: false,
defaultValue: false,
},
// "isPrivate" will be used to determine if the post should be visible only for the user's followers
// If "isLocal" is set to true and this to false, only users on the same server will be able to see the psot
isPrivate: {
type: "boolean",
required: false,
index: false,
defaultValue: false,
},
createdAt: {
type: "date",
required: true,
index: false
},
federationUrl: {
type: "string",
required: false,
index: true,
},
// This serves as a reference to the post on the original server this post came from
federationPostId: {
type: "string",
required: false,
index: true,
},
/**
* Base64-encoded Ed25519 detached signature produced client-side by
* the author's mnemonic-derived identity key. Covers the canonical
* post payload defined in `src/lib/identity/postSignature.ts`.
*
* Optional so federated/legacy posts that arrive without a per-user
* signature can still be stored, but locally-authored posts always
* have one — the createPost endpoint rejects requests that don't.
*/
authorSignature: {
type: "string",
required: false,
index: false,
},
}
},
follows: {
fields: {
followerId: {
type: "string",
required: true,
index: false,
},
followingId: {
type: "string",
required: true,
index: false,
},
accepted: {
type: "boolean",
required: true,
index: false,
defaultValue: false,
},
createdAt: {
type: "date",
required: true,
index: false
},
followerServerUrl: {
type: "string",
required: false,
index: true,
references: {
model: "serverRegistry",
field: "url"
}
},
followingServerUrl: {
type: "string",
required: false,
index: true,
references: {
model: "serverRegistry",
field: "url"
}
},
acknowledged: {
type: "boolean",
required: true,
index: false,
defaultValue: false,
},
}
},
deliveryJobs: {
fields: {
targetUrl: {
type: "string",
required: true,
index: false
},
// This could be encrypted, so we're not using a transform function to check for validity
payload: {
type: "string",
required: true,
index: false
},
attempts: {
type: "number",
required: true,
index: false,
defaultValue: 0,
},
lastAttemptedAt: {
type: "date",
required: false,
index: false,
},
nextAttemptAt: {
type: "date",
required: false,
index: false,
},
createdAt: {
type: "date",
required: true,
index: false
}
}
},
mutes: {
fields: {
userId: {
type: "string",
required: true,
index: false,
references: {
model: "user",
field: "id"
}
},
mutedUserId: {
type: "string",
required: true,
index: false,
references: {
model: "user",
field: "id"
}
},
createdAt: {
type: "date",
required: true,
index: false
}
}
},
blocks: {
fields: {
blockerId: {
type: "string",
required: true,
index: false,
references: {
model: "user",
field: "id"
}
},
blockedUserId: {
type: "string",
required: true,
index: false,
references: {
model: "user",
field: "id"
}
},
createdAt: {
type: "date",
required: true,
index: false
}
}
}
} satisfies BetterAuthPluginDBSchema