Product registry: catalogs, revisions, product info and libraries
CI / test (push) Successful in 43s

The hub registers catalogs (hidden until a moderator approves them), hands out upload tokens, and records the
revisions catalogs register after validating uploads: numbered, never changed, signed by the hub (its own
Ed25519 key, published at /.well-known/t5de-keys.json). Creators edit product info (descriptions cleaned by an
allowlist) and can retract revisions. People get products into their library, optionally staying on a
revision; the proxy asks which revision to load and where. Contract in docs/products.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
dhkatzandClaude Opus 5.5 committed 2026-10-29 18:53:52 -07:00
1 parent 7fea63fc4f
commit b4f6fce9df
15 files changed
+2883 -14

No files matched your search

+1
View File
@@ -11,6 +11,7 @@ import { fileURLToPath } from "node:url";
import * as schema from "./schema";
export { schema };
export type { FileEntry } from "./schema";
export * from "drizzle-orm";
const migrationsFolder = fileURLToPath(new URL("../drizzle", import.meta.url));
+100
View File
@@ -221,3 +221,103 @@ export const audit = pgTable(
},
(t) => [index("audit_at").on(t.at)],
);
// ---- Products (docs/products.md) ----
export type FileEntry = { name: string; sha256: string; size: number };
// The hub's own signing keys (Ed25519) for the tokens catalogs and proxies check. The newest is used.
export const signingKey = pgTable("signing_key", {
id: text("id").primaryKey(), // the JWK's kid
publicKey: jsonb("public_key").$type<Record<string, string>>().notNull(),
privateKey: jsonb("private_key").$type<Record<string, string>>().notNull(),
createdAt: createdAt(),
});
// A catalog: a server hosting product files. Hidden until a moderator approves it.
export const catalog = pgTable(
"catalog",
{
id: text("id").primaryKey(),
ownerId: text("owner_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
name: text("name").notNull(),
url: text("url").notNull(), // its public URL
contact: text("contact").notNull(), // takedown and legal contact
visibility: text("visibility").notNull().default("public"), // "public", "private"
membership: text("membership").notNull().default("open"), // "open", "approval", "invite"
status: text("status").notNull().default("pending"), // "pending", "approved", "rejected"
keyHash: text("key_hash").notNull(), // sha256 of its key, hex
createdAt: createdAt(),
},
(t) => [uniqueIndex("catalog_url").on(t.url)],
);
// A product: its info, owned by its creator. Its files are in revisions.
export const product = pgTable(
"product",
{
id: text("id").primaryKey(), // 10 of a-z2-7
creatorId: text("creator_id")
.notNull()
.references(() => user.id, { onDelete: "restrict" }),
name: text("name").notNull(),
description: text("description").notNull().default(""), // HTML, already cleaned
rating: text("rating").notNull(), // "general", "adult": the highest of its revisions, or raised by the creator
latest: integer("latest").notNull().default(0), // the newest revision's number
createdAt: createdAt(),
updatedAt: updatedAt(),
},
(t) => [index("product_creator").on(t.creatorId), index("product_created").on(t.createdAt)],
);
// A revision: a product's files. Never changes (but can be retracted).
export const productRevision = pgTable(
"product_revision",
{
productId: text("product_id")
.notNull()
.references(() => product.id, { onDelete: "cascade" }),
number: integer("number").notNull(),
files: jsonb("files").$type<FileEntry[]>().notNull(),
rating: text("rating").notNull(),
parent: bigint("parent", { mode: "number" }).notNull().default(0), // the IMVU product it derives from
notes: text("notes").notNull().default(""),
token: text("token").notNull(), // the hub's t5de-revision token
uploadedBy: text("uploaded_by").references(() => user.id, { onDelete: "set null" }),
retracted: boolean("retracted").notNull().default(false),
createdAt: createdAt(),
},
(t) => [primaryKey({ columns: [t.productId, t.number] })],
);
// Which catalogs host which revisions.
export const hosting = pgTable(
"hosting",
{
catalogId: text("catalog_id")
.notNull()
.references(() => catalog.id, { onDelete: "cascade" }),
productId: text("product_id").notNull(),
number: integer("number").notNull(),
createdAt: createdAt(),
},
(t) => [primaryKey({ columns: [t.catalogId, t.productId, t.number] }), index("hosting_product").on(t.productId)],
);
// A person's library: products they got, optionally staying on a revision.
export const library = pgTable(
"library",
{
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
productId: text("product_id")
.notNull()
.references(() => product.id, { onDelete: "cascade" }),
stayOn: integer("stay_on"),
addedAt: timestamp("added_at", { withTimezone: true }).notNull().defaultNow(),
},
(t) => [primaryKey({ columns: [t.userId, t.productId] })],
);