Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,23 @@ Auth challenge, verification, and refresh endpoints use Fastify rate limiting ba

Google auth routes are not registered as a default login path. With `GOOGLE_AUTH_ENABLED=false`, `/auth/google/*` returns a not-enabled response. Enabling Google currently exposes the guarded boundary only; Google OAuth account linking and automatic Hive provisioning remain deferred until secure non-custodial account creation and key delivery are specified.

Implemented world endpoints:

- `POST /worlds`: requires authentication, creates a mutable off-chain World Seed and initial `WorldBibleVersion` in one transaction, and assigns the authenticated user an active `FOUNDER` membership. The route never accepts a client-supplied founder or role and does not publish to Hive.
- `GET /worlds`: browses discoverable worlds with pagination and basic title/description, genre, and tone filters.
- `GET /worlds/:worldId`: returns one world with its founder, seed, and current latest World Bible version.
- `GET /worlds/:worldId/hub`: returns the World Hub aggregate for the frontend, including world seed, current bible preview, simple canon/proposal stats, and latest canon lore entry summaries.
- `PATCH /worlds/:worldId`: requires `EDIT_INITIAL_CANON` world permission and updates mutable off-chain world title/description, seed, and latest initial bible content only. It does not create proposals, canonize content, or publish to Hive.

Implemented contribution endpoints:

- `POST /worlds/:worldId/contributions`: requires authentication and `CREATE_LORE_DRAFT` world permission, creates an author-owned structured contribution draft for lore or story content, and optionally links it to a lore entry in the same world.
- `GET /worlds/:worldId/contributions`: requires authentication and lists only the authenticated writer's contributions in the world with page/pageSize pagination and optional status/kind filters.
- `GET /worlds/:worldId/contributions/:contributionId`: returns an authenticated writer's own contribution draft or submitted contribution without exposing other writers' private drafts.
- `PATCH /worlds/:worldId/contributions/:contributionId`: requires `EDIT_OWN_DRAFT`, updates only mutable draft fields, and rejects submitted contributions.
- `DELETE /worlds/:worldId/contributions/:contributionId`: requires `EDIT_OWN_DRAFT`, deletes only draft contributions, and never deletes submitted proposals.
- `POST /worlds/:worldId/contributions/:contributionId/submit`: requires `SUBMIT_PROPOSAL`, atomically locks the draft, creates exactly one submitted proposal with an immutable `proposedContent` snapshot, and does not publish to Hive.

---

# Reputation & Rewards
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
CREATE TABLE "WorldSeed" (
"id" TEXT NOT NULL,
"worldId" TEXT NOT NULL,
"premise" TEXT NOT NULL,
"genre" TEXT NOT NULL,
"tone" TEXT NOT NULL,
"mainConflict" TEXT NOT NULL,
"startingLocation" TEXT,
"firstCharacters" JSONB,
"firstFactions" JSONB,
"firstHistoricalEvent" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,

CONSTRAINT "WorldSeed_pkey" PRIMARY KEY ("id")
);

CREATE UNIQUE INDEX "WorldSeed_worldId_key" ON "WorldSeed"("worldId");
CREATE INDEX "WorldSeed_genre_idx" ON "WorldSeed"("genre");
CREATE INDEX "WorldSeed_tone_idx" ON "WorldSeed"("tone");

ALTER TABLE "WorldSeed" ADD CONSTRAINT "WorldSeed_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
-- CreateEnum
CREATE TYPE "ContributionKind" AS ENUM ('LORE', 'STORY');

-- CreateEnum
CREATE TYPE "ContributionStatus" AS ENUM ('DRAFT', 'SUBMITTED');

-- AlterEnum
ALTER TYPE "ProposalType" ADD VALUE 'ADD_STORY';

-- AlterEnum
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_UPDATED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_DELETED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'CONTRIBUTION_SUBMITTED';
ALTER TYPE "WorldAuditAction" ADD VALUE 'PROPOSAL_CREATED';

-- AlterTable
ALTER TABLE "Proposal" ADD COLUMN "contributionKind" "ContributionKind";

-- CreateTable
CREATE TABLE "ContributionDraft" (
"id" TEXT NOT NULL,
"worldId" TEXT NOT NULL,
"authorId" TEXT NOT NULL,
"targetLoreEntryId" TEXT,
"kind" "ContributionKind" NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT,
"content" JSONB NOT NULL,
"status" "ContributionStatus" NOT NULL DEFAULT 'DRAFT',
"proposalId" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"submittedAt" TIMESTAMP(3),

CONSTRAINT "ContributionDraft_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "ContributionDraft_proposalId_key" ON "ContributionDraft"("proposalId");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_idx" ON "ContributionDraft"("worldId");

-- CreateIndex
CREATE INDEX "ContributionDraft_authorId_idx" ON "ContributionDraft"("authorId");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_status_idx" ON "ContributionDraft"("worldId", "status");

-- CreateIndex
CREATE INDEX "ContributionDraft_worldId_authorId_status_idx" ON "ContributionDraft"("worldId", "authorId", "status");

-- CreateIndex
CREATE INDEX "ContributionDraft_targetLoreEntryId_idx" ON "ContributionDraft"("targetLoreEntryId");

-- CreateIndex
CREATE INDEX "Proposal_worldId_contributionKind_idx" ON "Proposal"("worldId", "contributionKind");

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_worldId_fkey" FOREIGN KEY ("worldId") REFERENCES "World"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_targetLoreEntryId_fkey" FOREIGN KEY ("targetLoreEntryId") REFERENCES "LoreEntry"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "ContributionDraft" ADD CONSTRAINT "ContributionDraft_proposalId_fkey" FOREIGN KEY ("proposalId") REFERENCES "Proposal"("id") ON DELETE SET NULL ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TYPE "LoreType" ADD VALUE IF NOT EXISTS 'QUEST';
ALTER TYPE "LoreType" ADD VALUE IF NOT EXISTS 'STORY';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_ENTRY_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_ENTRY_UPDATED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_ENTRY_DELETED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_RELATIONSHIP_CREATED';
ALTER TYPE "WorldAuditAction" ADD VALUE IF NOT EXISTS 'LORE_RELATIONSHIP_DELETED';
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
UPDATE "LoreEntry"
SET "loreType" = 'QUEST'
WHERE "loreType" = 'OTHER'
AND "status" <> 'PUBLISHED_CANON'
AND "content"->>'entityType' = 'QUEST';

UPDATE "LoreEntry"
SET "loreType" = 'STORY'
WHERE "loreType" = 'OTHER'
AND "status" <> 'PUBLISHED_CANON'
AND "content"->>'entityType' = 'STORY_CONTRIBUTION';
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- Support deterministic lore list and relation projections.
CREATE INDEX "LoreEntry_worldId_updatedAt_id_idx" ON "LoreEntry"("worldId", "updatedAt", "id");
CREATE INDEX "LoreRelationship_sourceId_updatedAt_id_idx" ON "LoreRelationship"("sourceId", "updatedAt", "id");
CREATE INDEX "LoreRelationship_targetId_updatedAt_id_idx" ON "LoreRelationship"("targetId", "updatedAt", "id");
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
WITH repaired AS (
UPDATE "LoreEntry"
SET "loreType" = CASE
WHEN "content"->>'entityType' = 'QUEST' THEN 'QUEST'::"LoreType"
WHEN "content"->>'entityType' = 'STORY_CONTRIBUTION' THEN 'STORY'::"LoreType"
ELSE "loreType"
END
WHERE "status" = 'PUBLISHED_CANON'
AND "loreType" = 'OTHER'
AND "content"->>'entityType' IN ('QUEST', 'STORY_CONTRIBUTION')
RETURNING
"id",
"worldId",
"loreType",
"content"->>'entityType' AS "entityType"
)
INSERT INTO "WorldAuditLog" (
"id",
"worldId",
"actorId",
"action",
"targetType",
"targetId",
"metadata",
"createdAt"
)
SELECT
gen_random_uuid()::text,
repaired."worldId",
NULL,
'LORE_ENTRY_UPDATED',
'LORE_ENTRY',
repaired."id",
jsonb_build_object(
'reason', 'Corrected a published canon lore type that still used OTHER after the QUEST/STORY type split.',
'migration', '20260810190000_repair_published_canon_quest_story_lore_types',
'loreType', repaired."loreType",
'entityType', repaired."entityType"
),
CURRENT_TIMESTAMP
FROM repaired
WHERE NOT EXISTS (
SELECT 1
FROM "WorldAuditLog"
WHERE "WorldAuditLog"."targetType" = 'LORE_ENTRY'
AND "WorldAuditLog"."targetId" = repaired."id"
AND "WorldAuditLog"."action" = 'LORE_ENTRY_UPDATED'
AND "WorldAuditLog"."metadata"->>'migration' =
'20260810190000_repair_published_canon_quest_story_lore_types'
);
Loading
Loading