diff --git a/src/services/extensions/v1/database.ts b/src/services/extensions/v1/database.ts index 42611ae..f9fefac 100644 --- a/src/services/extensions/v1/database.ts +++ b/src/services/extensions/v1/database.ts @@ -1,5 +1,11 @@ import { DatabaseResult, IDatabase } from "../../../lib/interfaces"; -import { Extension, Release, Author, Repository, sortReleasesDescending } from "./interfaces"; +import { + Extension, + Release, + Author, + Repository, + sortReleasesDescending +} from "./interfaces"; const SELECT_EXTENSIONS = ` SELECT e.id, e.type, e.author_id, @@ -7,7 +13,7 @@ const SELECT_EXTENSIONS = ` e.name, e.description, e.releases, e.website, e.license, e.icon_url, e.readme, e.source, e.version, e.download_url FROM extensions e - LEFT JOIN authors a ON e.author_id = a.id + LEFT JOIN developers a ON e.author_id = a.id `; export class ExtensionsDatabase { diff --git a/src/services/extensions/v2/db/migrations/0008_rename_authors_to_developers.sql b/src/services/extensions/v2/db/migrations/0008_rename_authors_to_developers.sql new file mode 100644 index 0000000..4bd2064 --- /dev/null +++ b/src/services/extensions/v2/db/migrations/0008_rename_authors_to_developers.sql @@ -0,0 +1,93 @@ +-- v2: rename "authors" to "developers" everywhere v2 owns the name. "Author" +-- implied solo/literary authorship, which never fit an entity that can be an +-- organization, gets moderated/approved, and can be transferred or claimed +-- like any other account. Now, while v2 is still internal-only, is the +-- cheapest this ever gets to fix. +-- +-- v1's read-only extension-listing API (src/services/extensions/v1) is +-- unaffected: its JSON response still calls this field "author" +-- (v1/interfaces.ts), and extensions.author_id (v1's own column) is left +-- as-is. Only the query in v1/database.ts that joins this table follows the +-- rename. + +ALTER TABLE authors RENAME TO developers; + +DROP INDEX IF EXISTS idx_authors_owner_unique; +CREATE UNIQUE INDEX IF NOT EXISTS idx_developers_owner_unique ON developers(owner_user_id); + +DROP INDEX IF EXISTS idx_authors_approved; +CREATE INDEX IF NOT EXISTS idx_developers_approved ON developers(approved_at); + +-- Not a hard FK (see 0001_add_v2_tables.sql), but renamed to match anyway. +ALTER TABLE extension_submissions RENAME COLUMN author_id TO developer_id; +DROP INDEX IF EXISTS idx_submissions_author; +CREATE INDEX IF NOT EXISTS idx_submissions_developer ON extension_submissions(developer_id); + +-- extension_submissions.payload is a JSON blob (SubmissionPayload), not a +-- SQL column, so the ALTER above doesn't touch it — every already-persisted +-- row (any status) still has the old `{"author": {...}, "extension": {...}}` +-- shape. Translate it in place so it matches the renamed +-- SubmissionPayloadSchema everywhere, not just for submissions created after +-- this migration. Without this, approve() on a pre-existing pending +-- submission dereferences payload.developer on an object that only has +-- .author, throwing and surfacing as a generic database error forever; list +-- responses would also keep serving the old shape for old rows. +UPDATE extension_submissions +SET payload = json_set( + json_remove(payload, '$.author'), + '$.developer', + json_extract(payload, '$.author') +) +WHERE json_extract(payload, '$.author') IS NOT NULL + AND json_extract(payload, '$.developer') IS NULL; + +ALTER TABLE author_history RENAME TO developer_history; +ALTER TABLE developer_history RENAME COLUMN author_id TO developer_id; + +DROP INDEX IF EXISTS idx_author_history_author_changed_at; +CREATE INDEX IF NOT EXISTS idx_developer_history_developer_changed_at + ON developer_history(developer_id, changed_at); + +DROP TRIGGER IF EXISTS trg_author_history_no_update; +CREATE TRIGGER IF NOT EXISTS trg_developer_history_no_update +BEFORE UPDATE ON developer_history +BEGIN + SELECT RAISE(ABORT, 'developer_history is append-only: rows cannot be updated'); +END; + +DROP TRIGGER IF EXISTS trg_author_history_no_delete; +CREATE TRIGGER IF NOT EXISTS trg_developer_history_no_delete +BEFORE DELETE ON developer_history +BEGIN + SELECT RAISE(ABORT, 'developer_history is append-only: rows cannot be deleted'); +END; + +ALTER TABLE author_transfers RENAME TO developer_transfers; +ALTER TABLE developer_transfers RENAME COLUMN author_id TO developer_id; + +DROP INDEX IF EXISTS idx_author_transfers_token; +CREATE UNIQUE INDEX IF NOT EXISTS idx_developer_transfers_token ON developer_transfers(token_hash); + +DROP INDEX IF EXISTS idx_author_transfers_pending; +CREATE UNIQUE INDEX IF NOT EXISTS idx_developer_transfers_pending + ON developer_transfers(developer_id) + WHERE accepted_at IS NULL AND revoked_at IS NULL; + +ALTER TABLE author_claims RENAME TO developer_claims; +ALTER TABLE developer_claims RENAME COLUMN author_id TO developer_id; + +DROP INDEX IF EXISTS idx_author_claims_author; +CREATE INDEX IF NOT EXISTS idx_developer_claims_developer ON developer_claims(developer_id); + +DROP INDEX IF EXISTS idx_author_claims_claimant; +CREATE INDEX IF NOT EXISTS idx_developer_claims_claimant ON developer_claims(claimant_id); + +DROP INDEX IF EXISTS idx_author_claims_pending_unique; +CREATE UNIQUE INDEX IF NOT EXISTS idx_developer_claims_pending_unique + ON developer_claims(developer_id, claimant_id) + WHERE status = 'pending'; + +DROP INDEX IF EXISTS idx_author_claims_pending_queue; +CREATE INDEX IF NOT EXISTS idx_developer_claims_pending_queue + ON developer_claims(created_at) + WHERE status = 'pending'; diff --git a/src/services/extensions/v2/authors-database.ts b/src/services/extensions/v2/developers-database.ts similarity index 69% rename from src/services/extensions/v2/authors-database.ts rename to src/services/extensions/v2/developers-database.ts index 7d53ff4..7106910 100644 --- a/src/services/extensions/v2/authors-database.ts +++ b/src/services/extensions/v2/developers-database.ts @@ -1,26 +1,28 @@ import { DatabaseResult, IDatabase } from "../../../lib/interfaces"; import { databaseError } from "./errors"; import { - Author, - AuthorClaim, - AuthorHistoryEntry, - AuthorProfile, - AuthorTransfer, - PendingAuthorClaim + Developer, + DeveloperClaim, + DeveloperHistoryEntry, + DeveloperProfile, + DeveloperTransfer, + PendingDeveloperClaim } from "./interfaces"; -// Matches the SQLite/D1 message for the idx_authors_owner_unique violation, -// which is how a lost race between two concurrent first-time PUT /authors/me -// requests (same caller, different ids) surfaces. +// Matches the SQLite/D1 message for the idx_developers_owner_unique +// violation, which is how a lost race between two concurrent first-time PUT +// /developers/me requests (same caller, different ids) surfaces. function isOwnerConflict(message: string | undefined): boolean { return !!message && /UNIQUE constraint failed.*owner_user_id/i.test(message); } -// Matches the SQLite/D1 message for the idx_author_claims_pending_unique +// Matches the SQLite/D1 message for the idx_developer_claims_pending_unique // violation, which is how a duplicate claim() call while one is already // pending surfaces. function isPendingClaimConflict(message: string | undefined): boolean { - return !!message && /UNIQUE constraint failed.*author_claims/i.test(message); + return ( + !!message && /UNIQUE constraint failed.*developer_claims/i.test(message) + ); } async function sha256Hex(input: string): Promise { @@ -44,10 +46,10 @@ function toSqliteDatetime(date: Date): string { return date.toISOString().slice(0, 19).replace("T", " "); } -function parseAuthorRow(row: Record): AuthorProfile { +function parseDeveloperRow(row: Record): DeveloperProfile { return { id: row.id as string, - type: row.type as AuthorProfile["type"], + type: row.type as DeveloperProfile["type"], name: row.name as string, URL: (row.url as string | null) ?? undefined, bio: (row.bio as string | null) ?? undefined, @@ -57,12 +59,12 @@ function parseAuthorRow(row: Record): AuthorProfile { }; } -function parseClaimRow(row: Record): AuthorClaim { +function parseClaimRow(row: Record): DeveloperClaim { return { id: row.id as string, - author_id: row.author_id as string, + developer_id: row.developer_id as string, claimant_id: row.claimant_id as string, - status: row.status as AuthorClaim["status"], + status: row.status as DeveloperClaim["status"], note: (row.note as string | null) ?? undefined, review_note: (row.review_note as string | null) ?? undefined, reviewer_id: (row.reviewer_id as string | null) ?? undefined, @@ -71,7 +73,7 @@ function parseClaimRow(row: Record): AuthorClaim { }; } -export class AuthorsDatabase { +export class DevelopersDatabase { private db: IDatabase; constructor(db: IDatabase) { @@ -80,17 +82,17 @@ export class AuthorsDatabase { async upsertOwn( userId: string, - author: Author - ): Promise> { + developer: Developer + ): Promise> { try { const existingOwn = await this.db - .prepare("SELECT * FROM authors WHERE owner_user_id = ?") + .prepare("SELECT * FROM developers WHERE owner_user_id = ?") .bind(userId) .first>(); const existingById = await this.db - .prepare("SELECT * FROM authors WHERE id = ?") - .bind(author.id) + .prepare("SELECT * FROM developers WHERE id = ?") + .bind(developer.id) .first>(); let mainStmt; @@ -98,31 +100,31 @@ export class AuthorsDatabase { if (existingById) { return { data: null, - error: { message: "Author id already exists", code: "CONFLICT" } + error: { message: "Developer id already exists", code: "CONFLICT" } }; } mainStmt = this.db .prepare( - `INSERT INTO authors (id, type, name, url, bio, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at) + `INSERT INTO developers (id, type, name, url, bio, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, NULL, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)` ) .bind( - author.id, - author.type, - author.name, - author.URL ?? null, - author.bio ?? null, - author.avatar_url ?? null, - author.contact_email ?? null, + developer.id, + developer.type, + developer.name, + developer.URL ?? null, + developer.bio ?? null, + developer.avatar_url ?? null, + developer.contact_email ?? null, userId ); } else { - if (author.id !== existingOwn.id) { + if (developer.id !== existingOwn.id) { return { data: null, error: { - message: "Author id cannot be changed", + message: "Developer id cannot be changed", code: "CONFLICT" } }; @@ -133,17 +135,17 @@ export class AuthorsDatabase { // approval no longer applies. Not worth diffing old vs. new values. mainStmt = this.db .prepare( - `UPDATE authors SET type = ?, name = ?, url = ?, bio = ?, avatar_url = ?, contact_email = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP + `UPDATE developers SET type = ?, name = ?, url = ?, bio = ?, avatar_url = ?, contact_email = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ?` ) .bind( - author.type, - author.name, - author.URL ?? null, - author.bio ?? null, - author.avatar_url ?? null, - author.contact_email ?? null, - author.id + developer.type, + developer.name, + developer.URL ?? null, + developer.bio ?? null, + developer.avatar_url ?? null, + developer.contact_email ?? null, + developer.id ); } @@ -156,15 +158,15 @@ export class AuthorsDatabase { const historyStmt = this.db .prepare( - `INSERT INTO author_history (id, author_id, type, name, url, changed_by, changed_at) + `INSERT INTO developer_history (id, developer_id, type, name, url, changed_by, changed_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)` ) .bind( crypto.randomUUID(), - author.id, - author.type, - author.name, - author.URL ?? null, + developer.id, + developer.type, + developer.name, + developer.URL ?? null, userId ); @@ -204,38 +206,38 @@ export class AuthorsDatabase { ); } - return this.getById(author.id); + return this.getById(developer.id); } catch (error) { return databaseError("upsertOwn", error); } } - private async getById(id: string): Promise> { + private async getById(id: string): Promise> { try { const row = await this.db - .prepare("SELECT * FROM authors WHERE id = ?") + .prepare("SELECT * FROM developers WHERE id = ?") .bind(id) .first>(); if (!row) { return { data: null, error: { - message: `Cannot find author by id: ${id}`, + message: `Cannot find developer by id: ${id}`, code: "NOT_FOUND" } }; } - return { data: parseAuthorRow(row), error: null }; + return { data: parseDeveloperRow(row), error: null }; } catch (error) { return databaseError("getById", error); } } - async listAll(): Promise> { + async listAll(): Promise> { let result; try { result = await this.db - .prepare("SELECT * FROM authors ORDER BY name") + .prepare("SELECT * FROM developers ORDER BY name") .all>(); } catch (error) { return databaseError("listAll", error); @@ -249,17 +251,17 @@ export class AuthorsDatabase { } return { - data: (result.results ?? []).map(parseAuthorRow), + data: (result.results ?? []).map(parseDeveloperRow), error: null }; } - async listUnapproved(): Promise> { + async listUnapproved(): Promise> { let result; try { result = await this.db .prepare( - "SELECT * FROM authors WHERE approved_at IS NULL ORDER BY created_at ASC" + "SELECT * FROM developers WHERE approved_at IS NULL ORDER BY created_at ASC" ) .all>(); } catch (error) { @@ -274,7 +276,7 @@ export class AuthorsDatabase { } return { - data: (result.results ?? []).map(parseAuthorRow), + data: (result.results ?? []).map(parseDeveloperRow), error: null }; } @@ -286,7 +288,7 @@ export class AuthorsDatabase { try { result = await this.db .prepare( - "UPDATE authors SET approved_at = CURRENT_TIMESTAMP WHERE id = ?" + "UPDATE developers SET approved_at = CURRENT_TIMESTAMP WHERE id = ?" ) .bind(id) .run(); @@ -304,7 +306,10 @@ export class AuthorsDatabase { if (!result.meta?.changes) { return { data: null, - error: { message: `Cannot find author by id: ${id}`, code: "NOT_FOUND" } + error: { + message: `Cannot find developer by id: ${id}`, + code: "NOT_FOUND" + } }; } @@ -312,8 +317,8 @@ export class AuthorsDatabase { } async listHistory( - authorId: string - ): Promise> { + developerId: string + ): Promise> { let result; try { result = await this.db @@ -321,11 +326,11 @@ export class AuthorsDatabase { // CURRENT_TIMESTAMP has only second resolution, so two writes in // the same second tie on changed_at; rowid (insertion order) // breaks the tie so "newest first" is never ambiguous. - `SELECT author_id, type, name, url, changed_by, changed_at - FROM author_history WHERE author_id = ? + `SELECT developer_id, type, name, url, changed_by, changed_at + FROM developer_history WHERE developer_id = ? ORDER BY changed_at DESC, rowid DESC` ) - .bind(authorId) + .bind(developerId) .all>(); } catch (error) { return databaseError("listHistory", error); @@ -340,8 +345,8 @@ export class AuthorsDatabase { return { data: (result.results ?? []).map((row) => ({ - author_id: row.author_id as string, - type: row.type as AuthorHistoryEntry["type"], + developer_id: row.developer_id as string, + type: row.type as DeveloperHistoryEntry["type"], name: row.name as string, URL: (row.url as string | null) ?? undefined, changed_by: row.changed_by as string, @@ -352,18 +357,18 @@ export class AuthorsDatabase { } // Shared by initiateTransfer/revokeTransfer: both are owner-only actions on - // an existing author, so both need the same NOT_FOUND/FORBIDDEN check. + // an existing developer, so both need the same NOT_FOUND/FORBIDDEN check. private async checkOwnership( - authorId: string, + developerId: string, userId: string ): Promise<{ code: "NOT_FOUND" | "FORBIDDEN"; message: string } | null> { const owner = await this.db - .prepare("SELECT owner_user_id FROM authors WHERE id = ?") - .bind(authorId) + .prepare("SELECT owner_user_id FROM developers WHERE id = ?") + .bind(developerId) .first<{ owner_user_id: string | null }>(); if (!owner) { - return { code: "NOT_FOUND", message: "Author not found" }; + return { code: "NOT_FOUND", message: "Developer not found" }; } if (owner.owner_user_id !== userId) { return { code: "FORBIDDEN", message: "You don't own this profile" }; @@ -372,9 +377,9 @@ export class AuthorsDatabase { } async initiateTransfer( - authorId: string, + developerId: string, userId: string - ): Promise> { + ): Promise> { try { const token = crypto.randomUUID().replace(/-/g, "") + @@ -396,28 +401,28 @@ export class AuthorsDatabase { // loses ownership between an up-front check and the write could // otherwise still slip the write through. Superseding any existing // pending transfer (rather than stacking up) keeps - // idx_author_transfers_pending satisfied without a separate cleanup + // idx_developer_transfers_pending satisfied without a separate cleanup // pass. const revokeStmt = this.db .prepare( - `UPDATE author_transfers SET revoked_at = CURRENT_TIMESTAMP - WHERE author_id = ? AND accepted_at IS NULL AND revoked_at IS NULL - AND EXISTS (SELECT 1 FROM authors WHERE authors.id = author_transfers.author_id AND authors.owner_user_id = ?)` + `UPDATE developer_transfers SET revoked_at = CURRENT_TIMESTAMP + WHERE developer_id = ? AND accepted_at IS NULL AND revoked_at IS NULL + AND EXISTS (SELECT 1 FROM developers WHERE developers.id = developer_transfers.developer_id AND developers.owner_user_id = ?)` ) - .bind(authorId, userId); + .bind(developerId, userId); const insertStmt = this.db .prepare( - `INSERT INTO author_transfers (id, author_id, token_hash, created_by, expires_at) + `INSERT INTO developer_transfers (id, developer_id, token_hash, created_by, expires_at) SELECT ?, ?, ?, ?, ? - WHERE EXISTS (SELECT 1 FROM authors WHERE id = ? AND owner_user_id = ?)` + WHERE EXISTS (SELECT 1 FROM developers WHERE id = ? AND owner_user_id = ?)` ) .bind( crypto.randomUUID(), - authorId, + developerId, tokenHash, userId, expiresAt, - authorId, + developerId, userId ); @@ -436,10 +441,10 @@ export class AuthorsDatabase { // The INSERT only writes a row when the ownership guard above passes, // so zero rows written means the caller doesn't currently own this - // author — a follow-up read distinguishes NOT_FOUND from FORBIDDEN for - // the response without reopening the race the guard closes. + // developer — a follow-up read distinguishes NOT_FOUND from FORBIDDEN + // for the response without reopening the race the guard closes. if (!results[1]?.meta?.changes) { - const ownershipError = await this.checkOwnership(authorId, userId); + const ownershipError = await this.checkOwnership(developerId, userId); return { data: null, error: ownershipError ?? { @@ -456,17 +461,17 @@ export class AuthorsDatabase { } async revokeTransfer( - authorId: string, + developerId: string, userId: string ): Promise> { try { const result = await this.db .prepare( - `UPDATE author_transfers SET revoked_at = CURRENT_TIMESTAMP - WHERE author_id = ? AND accepted_at IS NULL AND revoked_at IS NULL - AND EXISTS (SELECT 1 FROM authors WHERE authors.id = author_transfers.author_id AND authors.owner_user_id = ?)` + `UPDATE developer_transfers SET revoked_at = CURRENT_TIMESTAMP + WHERE developer_id = ? AND accepted_at IS NULL AND revoked_at IS NULL + AND EXISTS (SELECT 1 FROM developers WHERE developers.id = developer_transfers.developer_id AND developers.owner_user_id = ?)` ) - .bind(authorId, userId) + .bind(developerId, userId) .run(); if (!result.success) { @@ -477,18 +482,18 @@ export class AuthorsDatabase { } // Zero rows changed is ambiguous by itself (no pending transfer vs. - // not the owner vs. no such author), since the ownership guard is + // not the owner vs. no such developer), since the ownership guard is // folded into the write above rather than checked beforehand. A // follow-up read-only check distinguishes them for the response // without reopening the race that guard closes. if (!result.meta?.changes) { - const ownershipError = await this.checkOwnership(authorId, userId); + const ownershipError = await this.checkOwnership(developerId, userId); if (ownershipError) { return { data: null, error: ownershipError }; } } - return { data: { id: authorId, revoked: true }, error: null }; + return { data: { id: developerId, revoked: true }, error: null }; } catch (error) { return databaseError("revokeTransfer", error); } @@ -497,7 +502,7 @@ export class AuthorsDatabase { async acceptTransfer( token: string, userId: string - ): Promise> { + ): Promise> { try { const tokenHash = await sha256Hex(token); @@ -512,7 +517,7 @@ export class AuthorsDatabase { // rather than as two separate writes. Splitting them would leave a // window, after the claim commits but before ownership actually // moves, where the *former* owner's initiateTransfer call would still - // see itself as the current owner (per the authors row) and could + // see itself as the current owner (per the developers row) and could // mint a fresh, valid link for a profile that's already mid-handoff. // It would also mean a failure on the ownership write alone (e.g. the // recipient racing to create another profile) permanently burns the @@ -531,25 +536,25 @@ export class AuthorsDatabase { // a fresh claim, not replaying an old one. // // The claim's NOT EXISTS guard folds the self-accept case (accepting - // user already owns *this* author) and the already-owns-a-different- - // profile case into the same atomic decision, so the token is never - // consumed unless the accepting user is actually eligible. A plain - // check-then-act (SELECT the row, decide, then write) would let two - // concurrent accepts both read it as valid before either one wrote to - // it, making the token usable more than once. + // user already owns *this* developer) and the already-owns-a- + // different-profile case into the same atomic decision, so the token + // is never consumed unless the accepting user is actually eligible. A + // plain check-then-act (SELECT the row, decide, then write) would let + // two concurrent accepts both read it as valid before either one + // wrote to it, making the token usable more than once. const claimStmt = this.db .prepare( - `UPDATE author_transfers SET accepted_at = CURRENT_TIMESTAMP, accepted_by = ? + `UPDATE developer_transfers SET accepted_at = CURRENT_TIMESTAMP, accepted_by = ? WHERE token_hash = ? AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > CURRENT_TIMESTAMP - AND NOT EXISTS (SELECT 1 FROM authors WHERE owner_user_id = ?)` + AND NOT EXISTS (SELECT 1 FROM developers WHERE owner_user_id = ?)` ) .bind(userId, tokenHash, userId); - const updateAuthorStmt = this.db + const updateDeveloperStmt = this.db .prepare( - `UPDATE authors SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP + `UPDATE developers SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE changes() = 1 AND id = ( - SELECT author_id FROM author_transfers + SELECT developer_id FROM developer_transfers WHERE token_hash = ? AND accepted_by = ? AND accepted_at IS NOT NULL )` ) @@ -559,7 +564,7 @@ export class AuthorsDatabase { try { results = (await this.db.batch([ claimStmt, - updateAuthorStmt + updateDeveloperStmt ])) as Array<{ success: boolean; error?: string; @@ -604,7 +609,7 @@ export class AuthorsDatabase { // ownership guard rejected it — check which, for an accurate error. const stillPending = await this.db .prepare( - `SELECT 1 FROM author_transfers + `SELECT 1 FROM developer_transfers WHERE token_hash = ? AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > CURRENT_TIMESTAMP` ) .bind(tokenHash) @@ -630,9 +635,11 @@ export class AuthorsDatabase { } const transfer = await this.db - .prepare("SELECT author_id FROM author_transfers WHERE token_hash = ?") + .prepare( + "SELECT developer_id FROM developer_transfers WHERE token_hash = ?" + ) .bind(tokenHash) - .first<{ author_id: string }>(); + .first<{ developer_id: string }>(); if (!transfer) { return databaseError( "acceptTransfer", @@ -640,16 +647,18 @@ export class AuthorsDatabase { ); } - return this.getById(transfer.author_id); + return this.getById(transfer.developer_id); } catch (error) { return databaseError("acceptTransfer", error); } } - private async getClaimById(id: string): Promise> { + private async getClaimById( + id: string + ): Promise> { try { const row = await this.db - .prepare("SELECT * FROM author_claims WHERE id = ?") + .prepare("SELECT * FROM developer_claims WHERE id = ?") .bind(id) .first>(); if (!row) { @@ -667,21 +676,21 @@ export class AuthorsDatabase { } } - // Shared by claim/approveClaim once an author/eligibility-guarded write - // affects zero rows: distinguishes "no such author" from the two possible - // ownership conflicts for an accurate response, without reopening the - // race the guarded write already closed. + // Shared by claim/approveClaim once a developer/eligibility-guarded write + // affects zero rows: distinguishes "no such developer" from the two + // possible ownership conflicts for an accurate response, without + // reopening the race the guarded write already closed. private async claimIneligibilityError( - authorId: string + developerId: string ): Promise<{ code: "NOT_FOUND" | "CONFLICT"; message: string }> { - const author = await this.db - .prepare("SELECT owner_user_id FROM authors WHERE id = ?") - .bind(authorId) + const developer = await this.db + .prepare("SELECT owner_user_id FROM developers WHERE id = ?") + .bind(developerId) .first<{ owner_user_id: string | null }>(); - if (!author) { - return { code: "NOT_FOUND", message: "Author not found" }; + if (!developer) { + return { code: "NOT_FOUND", message: "Developer not found" }; } - if (author.owner_user_id !== null) { + if (developer.owner_user_id !== null) { return { code: "CONFLICT", message: "This profile is already owned" }; } return { @@ -691,27 +700,34 @@ export class AuthorsDatabase { } async claim( - authorId: string, + developerId: string, claimantId: string, note?: string - ): Promise> { + ): Promise> { try { const id = crypto.randomUUID(); let result; try { // Both eligibility checks are folded into the INSERT itself, rather // than a separate SELECT beforehand — a caller who loses eligibility - // (author gets claimed/transferred, or the caller picks up a + // (developer gets claimed/transferred, or the caller picks up a // different profile) between an up-front check and the write could // otherwise still slip a stale claim through. result = await this.db .prepare( - `INSERT INTO author_claims (id, author_id, claimant_id, note) + `INSERT INTO developer_claims (id, developer_id, claimant_id, note) SELECT ?, ?, ?, ? - WHERE EXISTS (SELECT 1 FROM authors WHERE id = ? AND owner_user_id IS NULL) - AND NOT EXISTS (SELECT 1 FROM authors WHERE owner_user_id = ?)` + WHERE EXISTS (SELECT 1 FROM developers WHERE id = ? AND owner_user_id IS NULL) + AND NOT EXISTS (SELECT 1 FROM developers WHERE owner_user_id = ?)` + ) + .bind( + id, + developerId, + claimantId, + note ?? null, + developerId, + claimantId ) - .bind(id, authorId, claimantId, note ?? null, authorId, claimantId) .run(); } catch (error) { if ( @@ -738,7 +754,7 @@ export class AuthorsDatabase { if (!result.meta?.changes) { return { data: null, - error: await this.claimIneligibilityError(authorId) + error: await this.claimIneligibilityError(developerId) }; } @@ -750,12 +766,12 @@ export class AuthorsDatabase { async listMyClaims( claimantId: string - ): Promise> { + ): Promise> { let result; try { result = await this.db .prepare( - "SELECT * FROM author_claims WHERE claimant_id = ? ORDER BY created_at DESC" + "SELECT * FROM developer_claims WHERE claimant_id = ? ORDER BY created_at DESC" ) .bind(claimantId) .all>(); @@ -776,13 +792,13 @@ export class AuthorsDatabase { }; } - async listPendingClaims(): Promise> { + async listPendingClaims(): Promise> { let result; try { result = await this.db .prepare( - `SELECT c.*, a.name AS author_name, a.type AS author_type - FROM author_claims c JOIN authors a ON a.id = c.author_id + `SELECT c.*, d.name AS developer_name, d.type AS developer_type + FROM developer_claims c JOIN developers d ON d.id = c.developer_id WHERE c.status = 'pending' ORDER BY c.created_at ASC` ) .all>(); @@ -800,8 +816,9 @@ export class AuthorsDatabase { return { data: (result.results ?? []).map((row) => ({ ...parseClaimRow(row), - author_name: row.author_name as string, - author_type: row.author_type as PendingAuthorClaim["author_type"] + developer_name: row.developer_name as string, + developer_type: + row.developer_type as PendingDeveloperClaim["developer_type"] })), error: null }; @@ -814,7 +831,7 @@ export class AuthorsDatabase { try { await this.db .prepare( - `UPDATE author_claims SET status = 'pending', reviewer_id = NULL, reviewed_at = NULL + `UPDATE developer_claims SET status = 'pending', reviewer_id = NULL, reviewed_at = NULL WHERE id = ?` ) .bind(id) @@ -827,7 +844,7 @@ export class AuthorsDatabase { async approveClaim( claimId: string, reviewerId: string - ): Promise> { + ): Promise> { const existing = await this.getClaimById(claimId); if (existing.error || !existing.data) { return { @@ -855,17 +872,17 @@ export class AuthorsDatabase { } try { - const author = await this.db - .prepare("SELECT owner_user_id FROM authors WHERE id = ?") - .bind(claim.author_id) + const developer = await this.db + .prepare("SELECT owner_user_id FROM developers WHERE id = ?") + .bind(claim.developer_id) .first<{ owner_user_id: string | null }>(); - if (!author) { + if (!developer) { return { data: null, - error: { message: "Author not found", code: "NOT_FOUND" } + error: { message: "Developer not found", code: "NOT_FOUND" } }; } - if (author.owner_user_id !== null) { + if (developer.owner_user_id !== null) { return { data: null, error: { message: "This profile is already owned", code: "CONFLICT" } @@ -873,7 +890,7 @@ export class AuthorsDatabase { } const conflict = await this.db - .prepare("SELECT 1 FROM authors WHERE owner_user_id = ?") + .prepare("SELECT 1 FROM developers WHERE owner_user_id = ?") .bind(claim.claimant_id) .first(); if (conflict) { @@ -895,7 +912,7 @@ export class AuthorsDatabase { try { claimResult = await this.db .prepare( - `UPDATE author_claims SET status = 'approved', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP + `UPDATE developer_claims SET status = 'approved', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'` ) .bind(reviewerId, claimId) @@ -919,29 +936,32 @@ export class AuthorsDatabase { // The ownership write is itself guarded by `owner_user_id IS NULL`, so a // second concurrent approval of a *different* pending claim on the same - // author (each claim id claims its own status row above, so both could - // reach this point) can't also move ownership — only the first to commit - // here wins, and the loser's authorStmt affects zero rows, caught below. - // rejectOthersStmt is gated on that same win via `changes() = 1`, so - // competing claims are only auto-rejected once ownership has actually - // moved, not whenever this batch merely runs. + // developer (each claim id claims its own status row above, so both + // could reach this point) can't also move ownership — only the first to + // commit here wins, and the loser's developerStmt affects zero rows, + // caught below. rejectOthersStmt is gated on that same win via + // `changes() = 1`, so competing claims are only auto-rejected once + // ownership has actually moved, not whenever this batch merely runs. let results; try { - const authorStmt = this.db + const developerStmt = this.db .prepare( - `UPDATE authors SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP + `UPDATE developers SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND owner_user_id IS NULL` ) - .bind(claim.claimant_id, claim.author_id); + .bind(claim.claimant_id, claim.developer_id); const rejectOthersStmt = this.db .prepare( - `UPDATE author_claims SET status = 'rejected', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP, + `UPDATE developer_claims SET status = 'rejected', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP, review_note = 'Another claim on this profile was approved' - WHERE changes() = 1 AND author_id = ? AND status = 'pending' AND id != ?` + WHERE changes() = 1 AND developer_id = ? AND status = 'pending' AND id != ?` ) - .bind(reviewerId, claim.author_id, claimId); + .bind(reviewerId, claim.developer_id, claimId); - results = (await this.db.batch([authorStmt, rejectOthersStmt])) as Array<{ + results = (await this.db.batch([ + developerStmt, + rejectOthersStmt + ])) as Array<{ success: boolean; error?: string; meta?: { changes?: number }; @@ -960,8 +980,8 @@ export class AuthorsDatabase { ); } - const [authorResult] = results; - if (!authorResult.meta?.changes) { + const [developerResult] = results; + if (!developerResult.meta?.changes) { await this.revertClaimToPending(claimId); return { data: null, @@ -972,19 +992,19 @@ export class AuthorsDatabase { }; } - return this.getById(claim.author_id); + return this.getById(claim.developer_id); } async rejectClaim( claimId: string, reviewerId: string, reviewNote: string - ): Promise> { + ): Promise> { let result; try { result = await this.db .prepare( - `UPDATE author_claims SET status = 'rejected', reviewer_id = ?, review_note = ?, reviewed_at = CURRENT_TIMESTAMP + `UPDATE developer_claims SET status = 'rejected', reviewer_id = ?, review_note = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'` ) .bind(reviewerId, reviewNote, claimId) diff --git a/src/services/extensions/v2/index.ts b/src/services/extensions/v2/index.ts index 6cf36df..4518ca2 100644 --- a/src/services/extensions/v2/index.ts +++ b/src/services/extensions/v2/index.ts @@ -6,15 +6,15 @@ import { Scalar } from "@scalar/hono-api-reference"; import { getPlatform } from "../../../lib/middleware"; import { getAuth, requireAuth } from "../../../lib/auth"; import { - AuthorClaimSchema, - AuthorHistoryEntrySchema, - AuthorProfileSchema, - AuthorSchema, - AuthorTransferSchema, ClaimNoteSchema, + DeveloperClaimSchema, + DeveloperHistoryEntrySchema, + DeveloperProfileSchema, + DeveloperSchema, + DeveloperTransferSchema, ErrorResponseSchema, IdParamSchema, - PendingAuthorClaimSchema, + PendingDeveloperClaimSchema, QueueQuerySchema, ReviewNoteOptionalSchema, ReviewNoteRequiredSchema, @@ -22,7 +22,7 @@ import { SubmissionSchema, TokenParamSchema } from "./interfaces"; -import { AuthorsDatabase } from "./authors-database"; +import { DevelopersDatabase } from "./developers-database"; import { SubmissionsDatabase } from "./submissions-database"; import { UsersDatabase } from "./users-database"; @@ -110,7 +110,7 @@ const createSubmissionRoute = createRoute({ }, 403: { content: { "application/json": { schema: ErrorResponseSchema } }, - description: "Caller does not own the target author or extension" + description: "Caller does not own the target developer or extension" }, 422: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -144,7 +144,7 @@ extensionsV2.openapi(createSubmissionRoute, async (c) => { const created = await db.create({ extensionId: ownership.data.extensionId, - authorId: ownership.data.authorId, + developerId: ownership.data.developerId, submittedBy: auth.userId, payload }); @@ -296,7 +296,7 @@ const approveRoute = createRoute({ } }, description: - "Submission approved and written through to the live extension/author" + "Submission approved and written through to the live extension/developer" }, 401: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -425,23 +425,23 @@ extensionsV2.openapi(rejectRoute, async (c) => { return c.json({ result: data }, 200); }); -const upsertOwnAuthorRoute = createRoute({ +const upsertOwnDeveloperRoute = createRoute({ method: "put", - path: "/authors/me", - tags: ["Authors"], + path: "/developers/me", + tags: ["Developers"], summary: "Create or update the caller's own developer profile", security: [{ Bearer: [] }], middleware: [requireAuth()] as const, request: { body: { - content: { "application/json": { schema: AuthorSchema } } + content: { "application/json": { schema: DeveloperSchema } } } }, responses: { 200: { content: { "application/json": { - schema: z.object({ result: AuthorProfileSchema }) + schema: z.object({ result: DeveloperProfileSchema }) } }, description: "Developer profile created or updated and usable immediately" @@ -453,7 +453,7 @@ const upsertOwnAuthorRoute = createRoute({ 409: { content: { "application/json": { schema: ErrorResponseSchema } }, description: - "Author id already taken by someone else, or id was changed on an existing profile" + "Developer id already taken by someone else, or id was changed on an existing profile" }, 422: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -466,11 +466,11 @@ const upsertOwnAuthorRoute = createRoute({ } }); -extensionsV2.openapi(upsertOwnAuthorRoute, async (c) => { +extensionsV2.openapi(upsertOwnDeveloperRoute, async (c) => { const auth = getAuth(c); const body = c.req.valid("json"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.upsertOwn(auth.userId, body); if (error || !data) { @@ -495,10 +495,10 @@ function statusFromOwnershipErrorCode(code?: string): 403 | 404 | 500 { return 500; } -const claimAuthorRoute = createRoute({ +const claimDeveloperRoute = createRoute({ method: "post", - path: "/authors/{id}/claim", - tags: ["Authors"], + path: "/developers/{id}/claim", + tags: ["Developers"], summary: "Request ownership of an unowned developer profile", security: [{ Bearer: [] }], middleware: [requireAuth()] as const, @@ -511,7 +511,9 @@ const claimAuthorRoute = createRoute({ responses: { 201: { content: { - "application/json": { schema: z.object({ result: AuthorClaimSchema }) } + "application/json": { + schema: z.object({ result: DeveloperClaimSchema }) + } }, description: "Claim created and pending moderator review" }, @@ -521,7 +523,7 @@ const claimAuthorRoute = createRoute({ }, 404: { content: { "application/json": { schema: ErrorResponseSchema } }, - description: "No author with that id" + description: "No developer with that id" }, 409: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -539,12 +541,12 @@ const claimAuthorRoute = createRoute({ } }); -extensionsV2.openapi(claimAuthorRoute, async (c) => { +extensionsV2.openapi(claimDeveloperRoute, async (c) => { const auth = getAuth(c); const { id } = c.req.valid("param"); const { note } = c.req.valid("json"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.claim(id, auth.userId, note); if (error || !data) { @@ -564,8 +566,8 @@ extensionsV2.openapi(claimAuthorRoute, async (c) => { const myClaimsRoute = createRoute({ method: "get", - path: "/authors/claims/mine", - tags: ["Authors"], + path: "/developers/claims/mine", + tags: ["Developers"], summary: "List the caller's own profile claims, in any status", security: [{ Bearer: [] }], middleware: [requireAuth()] as const, @@ -573,7 +575,7 @@ const myClaimsRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: z.array(AuthorClaimSchema) }) + schema: z.object({ result: z.array(DeveloperClaimSchema) }) } }, description: "The caller's claims" @@ -592,7 +594,7 @@ const myClaimsRoute = createRoute({ extensionsV2.openapi(myClaimsRoute, async (c) => { const auth = getAuth(c); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.listMyClaims(auth.userId); if (error || !data) { @@ -612,7 +614,7 @@ extensionsV2.openapi(myClaimsRoute, async (c) => { const pendingClaimsRoute = createRoute({ method: "get", - path: "/authors/claims", + path: "/developers/claims", tags: ["Moderation"], summary: "List pending profile claims", security: [{ Bearer: [] }], @@ -621,7 +623,7 @@ const pendingClaimsRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: z.array(PendingAuthorClaimSchema) }) + schema: z.object({ result: z.array(PendingDeveloperClaimSchema) }) } }, description: "Claims awaiting moderator review" @@ -643,7 +645,7 @@ const pendingClaimsRoute = createRoute({ extensionsV2.openapi(pendingClaimsRoute, async (c) => { const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.listPendingClaims(); if (error || !data) { @@ -663,7 +665,7 @@ extensionsV2.openapi(pendingClaimsRoute, async (c) => { const approveClaimRoute = createRoute({ method: "post", - path: "/authors/claims/{id}/approve", + path: "/developers/claims/{id}/approve", tags: ["Moderation"], summary: "Approve a pending profile claim", security: [{ Bearer: [] }], @@ -673,7 +675,7 @@ const approveClaimRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: AuthorProfileSchema }) + schema: z.object({ result: DeveloperProfileSchema }) } }, description: @@ -689,7 +691,7 @@ const approveClaimRoute = createRoute({ }, 404: { content: { "application/json": { schema: ErrorResponseSchema } }, - description: "No claim or author with that id" + description: "No claim or developer with that id" }, 409: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -711,7 +713,7 @@ extensionsV2.openapi(approveClaimRoute, async (c) => { const auth = getAuth(c); const { id } = c.req.valid("param"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.approveClaim(id, auth.userId); if (error || !data) { @@ -731,7 +733,7 @@ extensionsV2.openapi(approveClaimRoute, async (c) => { const rejectClaimRoute = createRoute({ method: "post", - path: "/authors/claims/{id}/reject", + path: "/developers/claims/{id}/reject", tags: ["Moderation"], summary: "Reject a pending profile claim", security: [{ Bearer: [] }], @@ -745,7 +747,9 @@ const rejectClaimRoute = createRoute({ responses: { 200: { content: { - "application/json": { schema: z.object({ result: AuthorClaimSchema }) } + "application/json": { + schema: z.object({ result: DeveloperClaimSchema }) + } }, description: "Claim rejected" }, @@ -777,7 +781,7 @@ extensionsV2.openapi(rejectClaimRoute, async (c) => { const { id } = c.req.valid("param"); const { review_note } = c.req.valid("json"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.rejectClaim(id, auth.userId, review_note); if (error || !data) { @@ -798,8 +802,8 @@ extensionsV2.openapi(rejectClaimRoute, async (c) => { const initiateTransferRoute = createRoute({ method: "post", - path: "/authors/{id}/transfer", - tags: ["Authors"], + path: "/developers/{id}/transfer", + tags: ["Developers"], summary: "Create a single-use link to hand this profile to another account", security: [{ Bearer: [] }], middleware: [requireAuth()] as const, @@ -808,7 +812,7 @@ const initiateTransferRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: AuthorTransferSchema }) + schema: z.object({ result: DeveloperTransferSchema }) } }, description: @@ -824,7 +828,7 @@ const initiateTransferRoute = createRoute({ }, 404: { content: { "application/json": { schema: ErrorResponseSchema } }, - description: "No author with that id" + description: "No developer with that id" }, 422: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -841,7 +845,7 @@ extensionsV2.openapi(initiateTransferRoute, async (c) => { const auth = getAuth(c); const { id } = c.req.valid("param"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.initiateTransfer(id, auth.userId); if (error || !data) { @@ -861,8 +865,8 @@ extensionsV2.openapi(initiateTransferRoute, async (c) => { const revokeTransferRoute = createRoute({ method: "post", - path: "/authors/{id}/transfer/revoke", - tags: ["Authors"], + path: "/developers/{id}/transfer/revoke", + tags: ["Developers"], summary: "Revoke this profile's pending transfer link, if any", security: [{ Bearer: [] }], middleware: [requireAuth()] as const, @@ -888,7 +892,7 @@ const revokeTransferRoute = createRoute({ }, 404: { content: { "application/json": { schema: ErrorResponseSchema } }, - description: "No author with that id" + description: "No developer with that id" }, 422: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -905,7 +909,7 @@ extensionsV2.openapi(revokeTransferRoute, async (c) => { const auth = getAuth(c); const { id } = c.req.valid("param"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.revokeTransfer(id, auth.userId); if (error || !data) { @@ -925,8 +929,8 @@ extensionsV2.openapi(revokeTransferRoute, async (c) => { const acceptTransferRoute = createRoute({ method: "post", - path: "/authors/transfers/{token}/accept", - tags: ["Authors"], + path: "/developers/transfers/{token}/accept", + tags: ["Developers"], summary: "Accept a developer profile transfer using its single-use token", security: [{ Bearer: [] }], middleware: [requireAuth()] as const, @@ -935,7 +939,7 @@ const acceptTransferRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: AuthorProfileSchema }) + schema: z.object({ result: DeveloperProfileSchema }) } }, description: "Profile is now owned by the caller" @@ -967,7 +971,7 @@ extensionsV2.openapi(acceptTransferRoute, async (c) => { const auth = getAuth(c); const { token } = c.req.valid("param"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.acceptTransfer(token, auth.userId); if (error || !data) { @@ -986,9 +990,9 @@ extensionsV2.openapi(acceptTransferRoute, async (c) => { return c.json({ result: data }, 200); }); -const allAuthorsRoute = createRoute({ +const allDevelopersRoute = createRoute({ method: "get", - path: "/authors", + path: "/developers", tags: ["Moderation"], summary: "List every developer profile, approved or not", security: [{ Bearer: [] }], @@ -997,7 +1001,7 @@ const allAuthorsRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: z.array(AuthorProfileSchema) }) + schema: z.object({ result: z.array(DeveloperProfileSchema) }) } }, description: "All developer profiles" @@ -1017,16 +1021,16 @@ const allAuthorsRoute = createRoute({ } }); -extensionsV2.openapi(allAuthorsRoute, async (c) => { +extensionsV2.openapi(allDevelopersRoute, async (c) => { const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.listAll(); if (error || !data) { return c.json( { error: { - message: error?.message ?? "Unable to load authors", + message: error?.message ?? "Unable to load developers", code: "DATABASE_ERROR" } }, @@ -1037,9 +1041,9 @@ extensionsV2.openapi(allAuthorsRoute, async (c) => { return c.json({ result: data }, 200); }); -const unapprovedAuthorsRoute = createRoute({ +const unapprovedDevelopersRoute = createRoute({ method: "get", - path: "/authors/unapproved", + path: "/developers/unapproved", tags: ["Moderation"], summary: "List developer profiles awaiting moderator review", security: [{ Bearer: [] }], @@ -1048,7 +1052,7 @@ const unapprovedAuthorsRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: z.array(AuthorProfileSchema) }) + schema: z.object({ result: z.array(DeveloperProfileSchema) }) } }, description: "Developer profiles not yet approved" @@ -1068,16 +1072,16 @@ const unapprovedAuthorsRoute = createRoute({ } }); -extensionsV2.openapi(unapprovedAuthorsRoute, async (c) => { +extensionsV2.openapi(unapprovedDevelopersRoute, async (c) => { const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.listUnapproved(); if (error || !data) { return c.json( { error: { - message: error?.message ?? "Unable to load unapproved authors", + message: error?.message ?? "Unable to load unapproved developers", code: "DATABASE_ERROR" } }, @@ -1088,9 +1092,9 @@ extensionsV2.openapi(unapprovedAuthorsRoute, async (c) => { return c.json({ result: data }, 200); }); -const approveAuthorRoute = createRoute({ +const approveDeveloperRoute = createRoute({ method: "post", - path: "/authors/{id}/approve", + path: "/developers/{id}/approve", tags: ["Moderation"], summary: "Mark a developer profile as reviewed/approved", security: [{ Bearer: [] }], @@ -1117,7 +1121,7 @@ const approveAuthorRoute = createRoute({ }, 404: { content: { "application/json": { schema: ErrorResponseSchema } }, - description: "No author with that id" + description: "No developer with that id" }, 422: { content: { "application/json": { schema: ErrorResponseSchema } }, @@ -1130,10 +1134,10 @@ const approveAuthorRoute = createRoute({ } }); -extensionsV2.openapi(approveAuthorRoute, async (c) => { +extensionsV2.openapi(approveDeveloperRoute, async (c) => { const { id } = c.req.valid("param"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.approve(id); if (error || !data) { @@ -1141,7 +1145,7 @@ extensionsV2.openapi(approveAuthorRoute, async (c) => { return c.json( { error: { - message: error?.message ?? "Unable to approve author", + message: error?.message ?? "Unable to approve developer", code: error?.code ?? "DATABASE_ERROR" } }, @@ -1152,9 +1156,9 @@ extensionsV2.openapi(approveAuthorRoute, async (c) => { return c.json({ result: data }, 200); }); -const authorHistoryRoute = createRoute({ +const developerHistoryRoute = createRoute({ method: "get", - path: "/authors/{id}/history", + path: "/developers/{id}/history", tags: ["Moderation"], summary: "List the write history of a developer profile", security: [{ Bearer: [] }], @@ -1164,7 +1168,7 @@ const authorHistoryRoute = createRoute({ 200: { content: { "application/json": { - schema: z.object({ result: z.array(AuthorHistoryEntrySchema) }) + schema: z.object({ result: z.array(DeveloperHistoryEntrySchema) }) } }, description: "Snapshots of the profile, newest first" @@ -1188,17 +1192,17 @@ const authorHistoryRoute = createRoute({ } }); -extensionsV2.openapi(authorHistoryRoute, async (c) => { +extensionsV2.openapi(developerHistoryRoute, async (c) => { const { id } = c.req.valid("param"); const platform = getPlatform(c); - const db = new AuthorsDatabase(platform.getDatabase("DB_EXTENSIONS")); + const db = new DevelopersDatabase(platform.getDatabase("DB_EXTENSIONS")); const { data, error } = await db.listHistory(id); if (error || !data) { return c.json( { error: { - message: error?.message ?? "Unable to load author history", + message: error?.message ?? "Unable to load developer history", code: error?.code ?? "DATABASE_ERROR" } }, diff --git a/src/services/extensions/v2/interfaces.ts b/src/services/extensions/v2/interfaces.ts index 4a94546..b6f2d54 100644 --- a/src/services/extensions/v2/interfaces.ts +++ b/src/services/extensions/v2/interfaces.ts @@ -29,9 +29,9 @@ const httpUrl = () => message: "must use http or https" }); -export const AuthorSchema = z +export const DeveloperSchema = z .object({ - id: lowercaseId("author"), + id: lowercaseId("developer"), type: z.enum(["user", "organization"]), name: z.string().min(1), URL: httpUrl().optional(), @@ -39,22 +39,22 @@ export const AuthorSchema = z avatar_url: httpUrl().optional(), contact_email: z.string().email().optional() }) - .openapi("Author"); + .openapi("Developer"); -export type Author = z.infer; +export type Developer = z.infer; // Submissions go through moderation and only ever touch identity fields — // profile fields (bio/avatar_url/contact_email) are direct-write-only via -// PUT /authors/me, so this schema rejects them instead of silently +// PUT /developers/me, so this schema rejects them instead of silently // accepting-then-dropping them when a submission is approved. -export const SubmissionAuthorSchema = AuthorSchema.pick({ +export const SubmissionDeveloperSchema = DeveloperSchema.pick({ id: true, type: true, name: true, URL: true }) .strict() - .openapi("SubmissionAuthor"); + .openapi("SubmissionDeveloper"); export const ReleaseSchema = z .object({ @@ -99,31 +99,31 @@ export const ExtensionPayloadSchema = z export const SubmissionPayloadSchema = z .object({ - author: SubmissionAuthorSchema, + developer: SubmissionDeveloperSchema, extension: ExtensionPayloadSchema }) .openapi("SubmissionPayload"); export type SubmissionPayload = z.infer; -export const AuthorProfileSchema = AuthorSchema.extend({ +export const DeveloperProfileSchema = DeveloperSchema.extend({ approved: z.boolean() -}).openapi("AuthorProfile"); +}).openapi("DeveloperProfile"); -export type AuthorProfile = z.infer; +export type DeveloperProfile = z.infer; -export const AuthorHistoryEntrySchema = z +export const DeveloperHistoryEntrySchema = z .object({ - author_id: z.string(), + developer_id: z.string(), type: z.enum(["user", "organization"]), name: z.string(), URL: httpUrl().optional(), changed_by: z.string(), changed_at: z.string() }) - .openapi("AuthorHistoryEntry"); + .openapi("DeveloperHistoryEntry"); -export type AuthorHistoryEntry = z.infer; +export type DeveloperHistoryEntry = z.infer; export const ReviewNoteOptionalSchema = z .object({ @@ -149,7 +149,7 @@ export const SubmissionSchema = z .object({ id: z.string(), extension_id: z.string().nullable(), - author_id: z.string(), + developer_id: z.string(), submitted_by: z.string(), status: SubmissionStatusSchema, payload: SubmissionPayloadSchema, @@ -188,19 +188,19 @@ export const TokenParamSchema = z.object({ }) }); -export const AuthorTransferSchema = z +export const DeveloperTransferSchema = z .object({ token: z.string(), expires_at: z.string() }) - .openapi("AuthorTransfer"); + .openapi("DeveloperTransfer"); -export type AuthorTransfer = z.infer; +export type DeveloperTransfer = z.infer; -export const AuthorClaimSchema = z +export const DeveloperClaimSchema = z .object({ id: z.string(), - author_id: z.string(), + developer_id: z.string(), claimant_id: z.string(), status: z.enum(["pending", "approved", "rejected"]), note: z.string().optional(), @@ -209,16 +209,16 @@ export const AuthorClaimSchema = z created_at: z.string(), reviewed_at: z.string().optional() }) - .openapi("AuthorClaim"); + .openapi("DeveloperClaim"); -export type AuthorClaim = z.infer; +export type DeveloperClaim = z.infer; -export const PendingAuthorClaimSchema = AuthorClaimSchema.extend({ - author_name: z.string(), - author_type: z.enum(["user", "organization"]) -}).openapi("PendingAuthorClaim"); +export const PendingDeveloperClaimSchema = DeveloperClaimSchema.extend({ + developer_name: z.string(), + developer_type: z.enum(["user", "organization"]) +}).openapi("PendingDeveloperClaim"); -export type PendingAuthorClaim = z.infer; +export type PendingDeveloperClaim = z.infer; export const ClaimNoteSchema = z .object({ diff --git a/src/services/extensions/v2/submissions-database.ts b/src/services/extensions/v2/submissions-database.ts index 8fc039b..5779d22 100644 --- a/src/services/extensions/v2/submissions-database.ts +++ b/src/services/extensions/v2/submissions-database.ts @@ -4,12 +4,12 @@ import { Submission, SubmissionPayload, SubmissionStatus } from "./interfaces"; interface OwnershipResolution { extensionId: string | null; - authorId: string; + developerId: string; } interface CreateInput { extensionId: string | null; - authorId: string; + developerId: string; submittedBy: string; payload: SubmissionPayload; } @@ -18,7 +18,7 @@ function parseSubmissionRow(row: Record): Submission { return { id: row.id as string, extension_id: (row.extension_id as string | null) ?? null, - author_id: row.author_id as string, + developer_id: row.developer_id as string, submitted_by: row.submitted_by as string, status: row.status as SubmissionStatus, payload: JSON.parse(row.payload as string) as SubmissionPayload, @@ -36,9 +36,10 @@ export class SubmissionsDatabase { this.db = db; } - // Edits require owning the extension's current author; the author named in - // the payload (create, or an edit naming a different author) must be owned - // by the caller if it already exists, or is free to claim if it doesn't. + // Edits require owning the extension's current developer; the developer + // named in the payload (create, or an edit naming a different developer) + // must be owned by the caller if it already exists, or is free to claim if + // it doesn't. async resolveOwnership( payload: SubmissionPayload, callerId: string @@ -54,16 +55,19 @@ export class SubmissionsDatabase { let extensionId: string | null = null; if (existingExtension) { - const existingAuthor = await this.db - .prepare("SELECT owner_user_id FROM authors WHERE id = ?") + const existingDeveloper = await this.db + .prepare("SELECT owner_user_id FROM developers WHERE id = ?") .bind(existingExtension.author_id) .first<{ owner_user_id: string | null }>(); - if (!existingAuthor || existingAuthor.owner_user_id !== callerId) { + if ( + !existingDeveloper || + existingDeveloper.owner_user_id !== callerId + ) { return { data: null, error: { - message: "You do not own the author of this extension", + message: "You do not own the developer of this extension", code: "FORBIDDEN" } }; @@ -72,24 +76,24 @@ export class SubmissionsDatabase { extensionId = existingExtension.id; } - const payloadAuthor = await this.db - .prepare("SELECT owner_user_id FROM authors WHERE id = ?") - .bind(payload.author.id) + const payloadDeveloper = await this.db + .prepare("SELECT owner_user_id FROM developers WHERE id = ?") + .bind(payload.developer.id) .first<{ owner_user_id: string | null }>(); - if (!payloadAuthor || payloadAuthor.owner_user_id !== callerId) { + if (!payloadDeveloper || payloadDeveloper.owner_user_id !== callerId) { return { data: null, error: { message: - "You do not own this author, or it doesn't exist yet — create a developer profile first", + "You do not own this developer, or it doesn't exist yet — create a developer profile first", code: "FORBIDDEN" } }; } return { - data: { extensionId, authorId: payload.author.id }, + data: { extensionId, developerId: payload.developer.id }, error: null }; } catch (error) { @@ -104,13 +108,13 @@ export class SubmissionsDatabase { try { result = await this.db .prepare( - `INSERT INTO extension_submissions (id, extension_id, author_id, submitted_by, status, payload) + `INSERT INTO extension_submissions (id, extension_id, developer_id, submitted_by, status, payload) VALUES (?, ?, ?, ?, 'pending', ?)` ) .bind( id, input.extensionId, - input.authorId, + input.developerId, input.submittedBy, JSON.stringify(input.payload) ) @@ -209,9 +213,9 @@ export class SubmissionsDatabase { // Best-effort compensation: if the write-through after a successful claim // fails, put the submission back to 'pending' rather than leaving it - // permanently 'approved' with no matching author/extension write. If this - // itself fails, the submission is stuck 'approved' and needs manual fixup — - // surfaced via the DATABASE_ERROR the caller already returns. + // permanently 'approved' with no matching developer/extension write. If + // this itself fails, the submission is stuck 'approved' and needs manual + // fixup — surfaced via the DATABASE_ERROR the caller already returns. private async revertToPending(id: string): Promise { try { await this.db @@ -337,7 +341,7 @@ export class SubmissionsDatabase { // Claim the transition atomically before writing anything through. If // this affects no rows, a concurrent approve/reject already won the - // race — report CONFLICT without having touched authors/extensions. + // race — report CONFLICT without having touched developers/extensions. let claim; try { claim = await this.db @@ -371,7 +375,7 @@ export class SubmissionsDatabase { ); } - const { author, extension } = submission.payload; + const { developer, extension } = submission.payload; // Edits must update the existing row even if it was stored under a // different case (e.g. legacy "Example" edited via "example") — using // the payload's id here would insert a second row instead. @@ -379,18 +383,18 @@ export class SubmissionsDatabase { let results; try { - const authorStmt = this.db + const developerStmt = this.db .prepare( - `INSERT INTO authors (id, type, name, url, owner_user_id) + `INSERT INTO developers (id, type, name, url, owner_user_id) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET type = excluded.type, name = excluded.name, url = excluded.url` ) .bind( - author.id, - author.type, - author.name, - author.URL ?? null, + developer.id, + developer.type, + developer.name, + developer.URL ?? null, submission.submitted_by ); @@ -409,7 +413,7 @@ export class SubmissionsDatabase { .bind( extensionId, extension.type, - author.id, + developer.id, extension.name, extension.description, JSON.stringify(extension.releases), @@ -422,7 +426,7 @@ export class SubmissionsDatabase { extension.download_url ); - results = (await this.db.batch([authorStmt, extensionStmt])) as Array<{ + results = (await this.db.batch([developerStmt, extensionStmt])) as Array<{ success: boolean; error?: string; }>; diff --git a/test/services/extensions/v2/index.test.ts b/test/services/extensions/v2/index.test.ts index 3cb2d33..c556646 100644 --- a/test/services/extensions/v2/index.test.ts +++ b/test/services/extensions/v2/index.test.ts @@ -28,13 +28,13 @@ async function authHeaders(sub: string): Promise> { function samplePayload(overrides?: { extensionId?: string; - authorId?: string; + developerId?: string; }) { return { - author: { - id: overrides?.authorId ?? "new-author", + developer: { + id: overrides?.developerId ?? "new-developer", type: "user", - name: "Some Author", + name: "Some Developer", URL: "https://example.com" }, extension: { @@ -60,20 +60,20 @@ function samplePayload(overrides?: { }; } -// Extension submissions now require the named author to already exist -// (created via PUT /authors/me) and be owned by the caller. -function seedAuthor(id: string, ownerUserId: string): void { - tables.authors.set(id, { +// Extension submissions now require the named developer to already exist +// (created via PUT /developers/me) and be owned by the caller. +function seedDeveloper(id: string, ownerUserId: string): void { + tables.developers.set(id, { id, type: "user", - name: "Author", + name: "Developer", url: null, owner_user_id: ownerUserId }); } -function seedUnownedAuthor(id: string, name = "Legacy Author"): void { - tables.authors.set(id, { +function seedUnownedDeveloper(id: string, name = "Legacy Developer"): void { + tables.developers.set(id, { id, type: "user", name, @@ -86,8 +86,8 @@ function seedUnownedAuthor(id: string, name = "Legacy Author"): void { } function seedOwnedExtension(): void { - tables.authors.set("owner-author", { - id: "owner-author", + tables.developers.set("owner-developer", { + id: "owner-developer", type: "user", name: "Owner", url: null, @@ -96,7 +96,7 @@ function seedOwnedExtension(): void { tables.extensions.set("existing-ext", { id: "existing-ext", type: "mod", - author_id: "owner-author", + author_id: "owner-developer", name: "Existing", description: "d", releases: "[]", @@ -157,11 +157,11 @@ async function put( return res; } -function sampleAuthor(overrides?: { id?: string; name?: string }) { +function sampleDeveloper(overrides?: { id?: string; name?: string }) { return { - id: overrides?.id ?? "dev-author", + id: overrides?.id ?? "dev-developer", type: "user", - name: overrides?.name ?? "Dev Author", + name: overrides?.name ?? "Dev Developer", URL: "https://example.com" }; } @@ -182,7 +182,7 @@ describe("Extensions API v2", () => { it("rejects an invalid payload", async () => { const headers = await authHeaders("user-1"); const res = await post("/extensions/v2/submissions", headers, { - author: {}, + developer: {}, extension: {} }); expect(res.status).toBe(422); @@ -190,21 +190,21 @@ describe("Extensions API v2", () => { expect(data.error.code).toBe("VALIDATION_ERROR"); }); - it("rejects profile fields (bio/avatar_url/contact_email) on a submission's author", async () => { - seedAuthor("new-author", "user-1"); + it("rejects profile fields (bio/avatar_url/contact_email) on a submission's developer", async () => { + seedDeveloper("new-developer", "user-1"); const headers = await authHeaders("user-1"); const payload = samplePayload(); const res = await post("/extensions/v2/submissions", headers, { ...payload, - author: { ...payload.author, bio: "Should not be accepted here" } + developer: { ...payload.developer, bio: "Should not be accepted here" } }); expect(res.status).toBe(422); expect(tables.extension_submissions.size).toBe(0); }); - it("creates a pending submission for a brand-new extension under an existing author", async () => { - seedAuthor("new-author", "user-1"); + it("creates a pending submission for a brand-new extension under an existing developer", async () => { + seedDeveloper("new-developer", "user-1"); const headers = await authHeaders("user-1"); const res = await post( "/extensions/v2/submissions", @@ -231,7 +231,10 @@ describe("Extensions API v2", () => { const res = await post( "/extensions/v2/submissions", headers, - samplePayload({ extensionId: "existing-ext", authorId: "owner-author" }) + samplePayload({ + extensionId: "existing-ext", + developerId: "owner-developer" + }) ); expect(res.status).toBe(403); @@ -245,7 +248,10 @@ describe("Extensions API v2", () => { const res = await post( "/extensions/v2/submissions", headers, - samplePayload({ extensionId: "existing-ext", authorId: "owner-author" }) + samplePayload({ + extensionId: "existing-ext", + developerId: "owner-developer" + }) ); expect(res.status).toBe(201); @@ -253,7 +259,7 @@ describe("Extensions API v2", () => { expect(stored.extension_id).toBe("existing-ext"); }); - it("rejects claiming an author already owned by someone else", async () => { + it("rejects claiming a developer already owned by someone else", async () => { seedOwnedExtension(); const headers = await authHeaders("intruder"); @@ -262,20 +268,20 @@ describe("Extensions API v2", () => { headers, samplePayload({ extensionId: "another-new-ext", - authorId: "owner-author" + developerId: "owner-developer" }) ); expect(res.status).toBe(403); }); - it("rejects naming an author id that doesn't exist at all", async () => { + it("rejects naming a developer id that doesn't exist at all", async () => { const headers = await authHeaders("user-1"); const res = await post( "/extensions/v2/submissions", headers, - samplePayload({ authorId: "no-such-author" }) + samplePayload({ developerId: "no-such-developer" }) ); expect(res.status).toBe(403); @@ -285,17 +291,17 @@ describe("Extensions API v2", () => { describe("GET /submissions/mine", () => { it("returns only the caller's own submissions", async () => { - seedAuthor("author-a", "user-1"); - seedAuthor("author-b", "user-2"); + seedDeveloper("developer-a", "user-1"); + seedDeveloper("developer-b", "user-2"); await post( "/extensions/v2/submissions", await authHeaders("user-1"), - samplePayload({ extensionId: "ext-a", authorId: "author-a" }) + samplePayload({ extensionId: "ext-a", developerId: "developer-a" }) ); await post( "/extensions/v2/submissions", await authHeaders("user-2"), - samplePayload({ extensionId: "ext-b", authorId: "author-b" }) + samplePayload({ extensionId: "ext-b", developerId: "developer-b" }) ); const res = await get( @@ -327,7 +333,7 @@ describe("Extensions API v2", () => { it("returns pending submissions for a moderator", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); await post( "/extensions/v2/submissions", await authHeaders("user-1"), @@ -348,7 +354,7 @@ describe("Extensions API v2", () => { describe("approve / reject", () => { it("reverts to pending if the write-through fails after a successful claim", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); const created = await post( "/extensions/v2/submissions", @@ -382,7 +388,7 @@ describe("Extensions API v2", () => { it("approves a submission and it becomes visible via the v1 read path", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); const created = await post( "/extensions/v2/submissions", @@ -402,17 +408,19 @@ describe("Extensions API v2", () => { }; expect(approvedBody.result.status).toBe("approved"); + // v1's read-only API keeps calling this field "author" — its JSON + // response shape is intentionally unchanged by the v2 rename. const v1Res = await get("/extensions/v1/new-ext", {}); expect(v1Res.status).toBe(200); const v1Body = (await v1Res.json()) as { result: { id: string; author: { id: string } }; }; expect(v1Body.result.id).toBe("new-ext"); - expect(v1Body.result.author.id).toBe("new-author"); + expect(v1Body.result.author.id).toBe("new-developer"); }); it("blocks non-moderators from approving", async () => { - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); const created = await post( "/extensions/v2/submissions", await authHeaders("user-1"), @@ -430,7 +438,7 @@ describe("Extensions API v2", () => { it("rejects approving a submission that is not pending", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); const created = await post( "/extensions/v2/submissions", await authHeaders("user-1"), @@ -454,8 +462,8 @@ describe("Extensions API v2", () => { }); it("updates the existing row instead of duplicating it when an edit's id differs only by case", async () => { - tables.authors.set("owner-author", { - id: "owner-author", + tables.developers.set("owner-developer", { + id: "owner-developer", type: "user", name: "Owner", url: null, @@ -465,7 +473,7 @@ describe("Extensions API v2", () => { tables.extensions.set("Existing-Ext", { id: "Existing-Ext", type: "mod", - author_id: "owner-author", + author_id: "owner-developer", name: "Existing", description: "d", releases: "[]", @@ -482,7 +490,10 @@ describe("Extensions API v2", () => { const created = await post( "/extensions/v2/submissions", await authHeaders("owner-1"), - samplePayload({ extensionId: "existing-ext", authorId: "owner-author" }) + samplePayload({ + extensionId: "existing-ext", + developerId: "owner-developer" + }) ); const { result } = (await created.json()) as { result: { id: string } }; @@ -500,7 +511,7 @@ describe("Extensions API v2", () => { it("requires a review_note to reject", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); const created = await post( "/extensions/v2/submissions", await authHeaders("user-1"), @@ -518,7 +529,7 @@ describe("Extensions API v2", () => { it("rejects a submission with a note", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); - seedAuthor("new-author", "user-1"); + seedDeveloper("new-developer", "user-1"); const created = await post( "/extensions/v2/submissions", await authHeaders("user-1"), @@ -538,80 +549,82 @@ describe("Extensions API v2", () => { }); }); - describe("PUT /authors/me", () => { - it("creates a new author profile, unapproved", async () => { + describe("PUT /developers/me", () => { + it("creates a new developer profile, unapproved", async () => { const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); expect(res.status).toBe(200); const data = (await res.json()) as { result: { approved: boolean } }; expect(data.result.approved).toBe(false); - const stored = tables.authors.get("dev-author"); + const stored = tables.developers.get("dev-developer"); expect(stored).toBeDefined(); expect(stored?.approved_at).toBeNull(); }); it("updates an existing profile, still unapproved", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor({ name: "Renamed Author" }) + sampleDeveloper({ name: "Renamed Developer" }) ); expect(res.status).toBe(200); const data = (await res.json()) as { result: { name: string; approved: boolean }; }; - expect(data.result.name).toBe("Renamed Author"); + expect(data.result.name).toBe("Renamed Developer"); expect(data.result.approved).toBe(false); - expect(tables.authors.get("dev-author")?.name).toBe("Renamed Author"); + expect(tables.developers.get("dev-developer")?.name).toBe( + "Renamed Developer" + ); }); it("only lets one of two concurrent first-time profile creations by the same caller win", async () => { const headers = await authHeaders("user-1"); const [resA, resB] = await Promise.all([ put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", headers, - sampleAuthor({ id: "author-a" }) + sampleDeveloper({ id: "developer-a" }) ), put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", headers, - sampleAuthor({ id: "author-b" }) + sampleDeveloper({ id: "developer-b" }) ) ]); const statuses = [resA.status, resB.status].sort(); expect(statuses).toEqual([200, 409]); - const ownedAuthors = [...tables.authors.values()].filter( + const ownedDevelopers = [...tables.developers.values()].filter( (a) => a.owner_user_id === "user-1" ); - expect(ownedAuthors).toHaveLength(1); + expect(ownedDevelopers).toHaveLength(1); }); it("rejects an id that already belongs to someone else", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-2"), - sampleAuthor() + sampleDeveloper() ); expect(res.status).toBe(409); @@ -619,15 +632,15 @@ describe("Extensions API v2", () => { it("rejects changing the id on an existing profile", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor({ id: "different-id" }) + sampleDeveloper({ id: "different-id" }) ); expect(res.status).toBe(409); @@ -635,13 +648,13 @@ describe("Extensions API v2", () => { it("clears approval when an approved profile is edited", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const approved = await post( - "/extensions/v2/authors/dev-author/approve", + "/extensions/v2/developers/dev-developer/approve", await authHeaders("mod-1") ); expect(approved.status).toBe(200); @@ -651,9 +664,9 @@ describe("Extensions API v2", () => { expect(approvedBody.result.approved).toBe(true); const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor({ name: "Edited Again" }) + sampleDeveloper({ name: "Edited Again" }) ); expect(res.status).toBe(200); @@ -663,8 +676,8 @@ describe("Extensions API v2", () => { it("round-trips bio, avatar_url, and contact_email", async () => { const headers = await authHeaders("user-1"); - const res = await put("/extensions/v2/authors/me", headers, { - ...sampleAuthor(), + const res = await put("/extensions/v2/developers/me", headers, { + ...sampleDeveloper(), bio: "I build FOSSBilling extensions.", avatar_url: "https://example.com/avatar.png", contact_email: "dev@example.com" @@ -682,7 +695,7 @@ describe("Extensions API v2", () => { expect(data.result.avatar_url).toBe("https://example.com/avatar.png"); expect(data.result.contact_email).toBe("dev@example.com"); - const stored = tables.authors.get("dev-author"); + const stored = tables.developers.get("dev-developer"); expect(stored?.bio).toBe("I build FOSSBilling extensions."); expect(stored?.avatar_url).toBe("https://example.com/avatar.png"); expect(stored?.contact_email).toBe("dev@example.com"); @@ -690,15 +703,15 @@ describe("Extensions API v2", () => { it("updates bio, avatar_url, and contact_email on an existing profile", async () => { const headers = await authHeaders("user-1"); - await put("/extensions/v2/authors/me", headers, { - ...sampleAuthor(), + await put("/extensions/v2/developers/me", headers, { + ...sampleDeveloper(), bio: "Old bio.", avatar_url: "https://example.com/old.png", contact_email: "old@example.com" }); - const res = await put("/extensions/v2/authors/me", headers, { - ...sampleAuthor(), + const res = await put("/extensions/v2/developers/me", headers, { + ...sampleDeveloper(), bio: "New bio.", avatar_url: "https://example.com/new.png", contact_email: "new@example.com" @@ -716,7 +729,7 @@ describe("Extensions API v2", () => { expect(data.result.avatar_url).toBe("https://example.com/new.png"); expect(data.result.contact_email).toBe("new@example.com"); - const stored = tables.authors.get("dev-author"); + const stored = tables.developers.get("dev-developer"); expect(stored?.bio).toBe("New bio."); expect(stored?.avatar_url).toBe("https://example.com/new.png"); expect(stored?.contact_email).toBe("new@example.com"); @@ -724,9 +737,9 @@ describe("Extensions API v2", () => { it("accepts a payload without bio, avatar_url, or contact_email", async () => { const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); expect(res.status).toBe(200); @@ -743,27 +756,30 @@ describe("Extensions API v2", () => { }); }); - describe("author moderation", () => { - it("approves an author and removes it from the unapproved list", async () => { + describe("developer moderation", () => { + it("approves a developer and removes it from the unapproved list", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const approve = await post( - "/extensions/v2/authors/dev-author/approve", + "/extensions/v2/developers/dev-developer/approve", await authHeaders("mod-1") ); expect(approve.status).toBe(200); const approveBody = (await approve.json()) as { result: { id: string; approved: boolean }; }; - expect(approveBody.result).toEqual({ id: "dev-author", approved: true }); + expect(approveBody.result).toEqual({ + id: "dev-developer", + approved: true + }); const unapproved = await get( - "/extensions/v2/authors/unapproved", + "/extensions/v2/developers/unapproved", await authHeaders("mod-1") ); expect(unapproved.status).toBe(200); @@ -771,47 +787,47 @@ describe("Extensions API v2", () => { result: Array<{ id: string }>; }; expect(unapprovedBody.result.map((a) => a.id)).not.toContain( - "dev-author" + "dev-developer" ); }); - it("404s approving a nonexistent author", async () => { + it("404s approving a nonexistent developer", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const res = await post( - "/extensions/v2/authors/no-such-author/approve", + "/extensions/v2/developers/no-such-developer/approve", await authHeaders("mod-1") ); expect(res.status).toBe(404); }); - it("blocks non-moderators from listing unapproved authors", async () => { + it("blocks non-moderators from listing unapproved developers", async () => { const res = await get( - "/extensions/v2/authors/unapproved", + "/extensions/v2/developers/unapproved", await authHeaders("user-1") ); expect(res.status).toBe(403); }); - it("lists every author, approved and unapproved", async () => { + it("lists every developer, approved and unapproved", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-2"), - sampleAuthor({ id: "other-author", name: "Other Author" }) + sampleDeveloper({ id: "other-developer", name: "Other Developer" }) ); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); await post( - "/extensions/v2/authors/dev-author/approve", + "/extensions/v2/developers/dev-developer/approve", await authHeaders("mod-1") ); const res = await get( - "/extensions/v2/authors", + "/extensions/v2/developers", await authHeaders("mod-1") ); expect(res.status).toBe(200); @@ -819,83 +835,83 @@ describe("Extensions API v2", () => { result: Array<{ id: string; approved: boolean }>; }; expect(body.result.map((a) => a.id).sort()).toEqual([ - "dev-author", - "other-author" + "dev-developer", + "other-developer" ]); - expect(body.result.find((a) => a.id === "dev-author")?.approved).toBe( + expect(body.result.find((a) => a.id === "dev-developer")?.approved).toBe( true ); - expect(body.result.find((a) => a.id === "other-author")?.approved).toBe( - false - ); + expect( + body.result.find((a) => a.id === "other-developer")?.approved + ).toBe(false); }); - it("blocks non-moderators from listing all authors", async () => { + it("blocks non-moderators from listing all developers", async () => { const res = await get( - "/extensions/v2/authors", + "/extensions/v2/developers", await authHeaders("user-1") ); expect(res.status).toBe(403); }); - it("blocks non-moderators from approving authors", async () => { + it("blocks non-moderators from approving developers", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await post( - "/extensions/v2/authors/dev-author/approve", + "/extensions/v2/developers/dev-developer/approve", await authHeaders("user-1") ); expect(res.status).toBe(403); }); }); - describe("GET /authors/{id}/history", () => { + describe("GET /developers/{id}/history", () => { it("records a history entry for a newly created profile", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/authors/dev-author/history", + "/extensions/v2/developers/dev-developer/history", await authHeaders("mod-1") ); expect(res.status).toBe(200); const data = (await res.json()) as { result: Array<{ - author_id: string; + developer_id: string; name: string; changed_by: string; }>; }; expect(data.result).toHaveLength(1); - expect(data.result[0].author_id).toBe("dev-author"); - expect(data.result[0].name).toBe("Dev Author"); + expect(data.result[0].developer_id).toBe("dev-developer"); + expect(data.result[0].name).toBe("Dev Developer"); expect(data.result[0].changed_by).toBe("user-1"); }); it("orders entries newest-first and snapshots each write", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor({ name: "Original Name" }) + sampleDeveloper({ name: "Original Name" }) ); await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor({ name: "Edited Name" }) + sampleDeveloper({ name: "Edited Name" }) ); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/authors/dev-author/history", + "/extensions/v2/developers/dev-developer/history", await authHeaders("mod-1") ); @@ -908,11 +924,11 @@ describe("Extensions API v2", () => { expect(data.result[1].name).toBe("Original Name"); }); - it("returns an empty array for an author with no history", async () => { + it("returns an empty array for a developer with no history", async () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const res = await get( - "/extensions/v2/authors/no-such-author/history", + "/extensions/v2/developers/no-such-developer/history", await authHeaders("mod-1") ); @@ -923,13 +939,13 @@ describe("Extensions API v2", () => { it("blocks non-moderators", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await get( - "/extensions/v2/authors/dev-author/history", + "/extensions/v2/developers/dev-developer/history", await authHeaders("user-1") ); expect(res.status).toBe(403); @@ -937,21 +953,21 @@ describe("Extensions API v2", () => { it("does not record history for a rejected write (id already taken)", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-2"), - sampleAuthor() + sampleDeveloper() ); expect(res.status).toBe(409); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const history = await get( - "/extensions/v2/authors/dev-author/history", + "/extensions/v2/developers/dev-developer/history", await authHeaders("mod-1") ); const data = (await history.json()) as { result: unknown[] }; @@ -959,16 +975,16 @@ describe("Extensions API v2", () => { }); }); - describe("author transfers", () => { + describe("developer transfers", () => { it("initiating a second transfer revokes the first token", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const first = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); expect(first.status).toBe(200); @@ -976,13 +992,13 @@ describe("Extensions API v2", () => { .result.token; const second = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); expect(second.status).toBe(200); const acceptFirst = await post( - `/extensions/v2/authors/transfers/${firstToken}/accept`, + `/extensions/v2/developers/transfers/${firstToken}/accept`, await authHeaders("user-2") ); expect(acceptFirst.status).toBe(404); @@ -990,108 +1006,118 @@ describe("Extensions API v2", () => { it("accepts a valid token, transferring ownership and clearing approval", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); await post( - "/extensions/v2/authors/dev-author/approve", + "/extensions/v2/developers/dev-developer/approve", await authHeaders("mod-1") ); const initiate = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - `/extensions/v2/authors/transfers/${token}/accept`, + `/extensions/v2/developers/transfers/${token}/accept`, await authHeaders("user-2") ); expect(accept.status).toBe(200); const accepted = (await accept.json()) as { result: { id: string; approved: boolean }; }; - expect(accepted.result.id).toBe("dev-author"); + expect(accepted.result.id).toBe("dev-developer"); expect(accepted.result.approved).toBe(false); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-2"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-2" + ); const acceptAgain = await post( - `/extensions/v2/authors/transfers/${token}/accept`, + `/extensions/v2/developers/transfers/${token}/accept`, await authHeaders("user-3") ); expect(acceptAgain.status).toBe(404); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-2"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-2" + ); }); it("does not let replaying an already-used token reassign ownership away from a later owner", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const initiate1 = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token1 = ((await initiate1.json()) as { result: { token: string } }) .result.token; const accept1 = await post( - `/extensions/v2/authors/transfers/${token1}/accept`, + `/extensions/v2/developers/transfers/${token1}/accept`, await authHeaders("user-2") ); expect(accept1.status).toBe(200); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-2"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-2" + ); - // dev-author is legitimately handed off again, to a third user. + // dev-developer is legitimately handed off again, to a third user. const initiate2 = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-2") ); const token2 = ((await initiate2.json()) as { result: { token: string } }) .result.token; const accept2 = await post( - `/extensions/v2/authors/transfers/${token2}/accept`, + `/extensions/v2/developers/transfers/${token2}/accept`, await authHeaders("user-3") ); expect(accept2.status).toBe(200); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-3"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-3" + ); // Replaying the *first* (already-used) token, by the same user who // originally accepted it, must not silently reassign ownership back. const replay = await post( - `/extensions/v2/authors/transfers/${token1}/accept`, + `/extensions/v2/developers/transfers/${token1}/accept`, await authHeaders("user-2") ); expect(replay.status).toBe(404); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-3"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-3" + ); }); it("rejects an expired token", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const initiate = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; - for (const transfer of tables.author_transfers.values()) { + for (const transfer of tables.developer_transfers.values()) { transfer.expires_at = "2000-01-01 00:00:00"; } const accept = await post( - `/extensions/v2/authors/transfers/${token}/accept`, + `/extensions/v2/developers/transfers/${token}/accept`, await authHeaders("user-2") ); expect(accept.status).toBe(404); @@ -1099,26 +1125,26 @@ describe("Extensions API v2", () => { it("rejects a revoked token", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const initiate = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const revoke = await post( - "/extensions/v2/authors/dev-author/transfer/revoke", + "/extensions/v2/developers/dev-developer/transfer/revoke", await authHeaders("user-1") ); expect(revoke.status).toBe(200); const accept = await post( - `/extensions/v2/authors/transfers/${token}/accept`, + `/extensions/v2/developers/transfers/${token}/accept`, await authHeaders("user-2") ); expect(accept.status).toBe(404); @@ -1126,62 +1152,66 @@ describe("Extensions API v2", () => { it("rejects acceptance by a user who already owns a different profile", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-2"), - sampleAuthor({ id: "other-author", name: "Other Author" }) + sampleDeveloper({ id: "other-developer", name: "Other Developer" }) ); const initiate = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - `/extensions/v2/authors/transfers/${token}/accept`, + `/extensions/v2/developers/transfers/${token}/accept`, await authHeaders("user-2") ); expect(accept.status).toBe(409); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-1"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-1" + ); }); it("rejects the current owner accepting their own transfer link", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const initiate = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const token = ((await initiate.json()) as { result: { token: string } }) .result.token; const accept = await post( - `/extensions/v2/authors/transfers/${token}/accept`, + `/extensions/v2/developers/transfers/${token}/accept`, await authHeaders("user-1") ); expect(accept.status).toBe(409); - expect(tables.authors.get("dev-author")?.owner_user_id).toBe("user-1"); + expect(tables.developers.get("dev-developer")?.owner_user_id).toBe( + "user-1" + ); }); it("blocks a non-owner from initiating a transfer", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("intruder") ); expect(res.status).toBe(403); @@ -1189,29 +1219,29 @@ describe("Extensions API v2", () => { it("blocks a non-owner from revoking a transfer", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); await post( - "/extensions/v2/authors/dev-author/transfer", + "/extensions/v2/developers/dev-developer/transfer", await authHeaders("user-1") ); const res = await post( - "/extensions/v2/authors/dev-author/transfer/revoke", + "/extensions/v2/developers/dev-developer/transfer/revoke", await authHeaders("intruder") ); expect(res.status).toBe(403); }); }); - describe("author claims", () => { - it("lets a user claim an unowned author, visible to the claimant and moderators", async () => { - seedUnownedAuthor("legacy-author"); + describe("developer claims", () => { + it("lets a user claim an unowned developer, visible to the claimant and moderators", async () => { + seedUnownedDeveloper("legacy-developer"); const res = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), { note: "I'm the maintainer, see github.com/x" } ); @@ -1219,7 +1249,7 @@ describe("Extensions API v2", () => { const created = (await res.json()) as { result: { id: string } }; const mine = await get( - "/extensions/v2/authors/claims/mine", + "/extensions/v2/developers/claims/mine", await authHeaders("user-1") ); expect(mine.status).toBe(200); @@ -1231,26 +1261,26 @@ describe("Extensions API v2", () => { tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const pending = await get( - "/extensions/v2/authors/claims", + "/extensions/v2/developers/claims", await authHeaders("mod-1") ); expect(pending.status).toBe(200); const pendingData = (await pending.json()) as { - result: Array<{ id: string; author_name: string }>; + result: Array<{ id: string; developer_name: string }>; }; expect(pendingData.result.map((c) => c.id)).toEqual([created.result.id]); - expect(pendingData.result[0].author_name).toBe("Legacy Author"); + expect(pendingData.result[0].developer_name).toBe("Legacy Developer"); }); - it("rejects claiming an author that already has an owner", async () => { + it("rejects claiming a developer that already has an owner", async () => { await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await post( - "/extensions/v2/authors/dev-author/claim", + "/extensions/v2/developers/dev-developer/claim", await authHeaders("user-2"), {} ); @@ -1258,34 +1288,34 @@ describe("Extensions API v2", () => { }); it("does not create a duplicate row for a second claim while one is already pending", async () => { - seedUnownedAuthor("legacy-author"); + seedUnownedDeveloper("legacy-developer"); const first = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); expect(first.status).toBe(201); const second = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); expect(second.status).toBe(409); - expect(tables.author_claims.size).toBe(1); + expect(tables.developer_claims.size).toBe(1); }); it("rejects a claim from a user who already owns a different profile", async () => { - seedUnownedAuthor("legacy-author"); + seedUnownedDeveloper("legacy-developer"); await put( - "/extensions/v2/authors/me", + "/extensions/v2/developers/me", await authHeaders("user-1"), - sampleAuthor() + sampleDeveloper() ); const res = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1293,11 +1323,11 @@ describe("Extensions API v2", () => { }); it("approving a claim transfers ownership and auto-rejects competing claims", async () => { - seedUnownedAuthor("legacy-author"); + seedUnownedDeveloper("legacy-developer"); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const claim1 = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1305,7 +1335,7 @@ describe("Extensions API v2", () => { .result.id; const claim2 = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-2"), {} ); @@ -1313,30 +1343,32 @@ describe("Extensions API v2", () => { .result.id; const approve = await post( - `/extensions/v2/authors/claims/${claim1Id}/approve`, + `/extensions/v2/developers/claims/${claim1Id}/approve`, await authHeaders("mod-1") ); expect(approve.status).toBe(200); const approved = (await approve.json()) as { result: { id: string; approved: boolean }; }; - expect(approved.result.id).toBe("legacy-author"); + expect(approved.result.id).toBe("legacy-developer"); expect(approved.result.approved).toBe(false); - expect(tables.authors.get("legacy-author")?.owner_user_id).toBe("user-1"); + expect(tables.developers.get("legacy-developer")?.owner_user_id).toBe( + "user-1" + ); - const rejectedClaim = tables.author_claims.get(claim2Id); + const rejectedClaim = tables.developer_claims.get(claim2Id); expect(rejectedClaim?.status).toBe("rejected"); expect(rejectedClaim?.review_note).toBe( "Another claim on this profile was approved" ); }); - it("lets a moderator reject a claim with a review note, leaving the author unowned", async () => { - seedUnownedAuthor("legacy-author"); + it("lets a moderator reject a claim with a review note, leaving the developer unowned", async () => { + seedUnownedDeveloper("legacy-developer"); tables.users.set("mod-1", { id: "mod-1", is_moderator: 1 }); const claim = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1344,7 +1376,7 @@ describe("Extensions API v2", () => { .result.id; const reject = await post( - `/extensions/v2/authors/claims/${claimId}/reject`, + `/extensions/v2/developers/claims/${claimId}/reject`, await authHeaders("mod-1"), { review_note: "Not enough evidence of maintainership" } ); @@ -1356,13 +1388,15 @@ describe("Extensions API v2", () => { expect(rejected.result.review_note).toBe( "Not enough evidence of maintainership" ); - expect(tables.authors.get("legacy-author")?.owner_user_id).toBeNull(); + expect( + tables.developers.get("legacy-developer")?.owner_user_id + ).toBeNull(); }); it("blocks non-moderators from the claims queue and review routes", async () => { - seedUnownedAuthor("legacy-author"); + seedUnownedDeveloper("legacy-developer"); const claim = await post( - "/extensions/v2/authors/legacy-author/claim", + "/extensions/v2/developers/legacy-developer/claim", await authHeaders("user-1"), {} ); @@ -1370,19 +1404,19 @@ describe("Extensions API v2", () => { .result.id; const queue = await get( - "/extensions/v2/authors/claims", + "/extensions/v2/developers/claims", await authHeaders("intruder") ); expect(queue.status).toBe(403); const approve = await post( - `/extensions/v2/authors/claims/${claimId}/approve`, + `/extensions/v2/developers/claims/${claimId}/approve`, await authHeaders("intruder") ); expect(approve.status).toBe(403); const reject = await post( - `/extensions/v2/authors/claims/${claimId}/reject`, + `/extensions/v2/developers/claims/${claimId}/reject`, await authHeaders("intruder"), { review_note: "no" } ); @@ -1406,17 +1440,17 @@ describe("Extensions API v2", () => { "/submissions/queue", "/submissions/{id}/approve", "/submissions/{id}/reject", - "/authors/me", - "/authors/unapproved", - "/authors/{id}/approve", - "/authors/{id}/transfer", - "/authors/{id}/transfer/revoke", - "/authors/transfers/{token}/accept", - "/authors/{id}/claim", - "/authors/claims/mine", - "/authors/claims", - "/authors/claims/{id}/approve", - "/authors/claims/{id}/reject" + "/developers/me", + "/developers/unapproved", + "/developers/{id}/approve", + "/developers/{id}/transfer", + "/developers/{id}/transfer/revoke", + "/developers/transfers/{token}/accept", + "/developers/{id}/claim", + "/developers/claims/mine", + "/developers/claims", + "/developers/claims/{id}/approve", + "/developers/claims/{id}/reject" ]) ); }); diff --git a/test/services/extensions/v2/mock-db.ts b/test/services/extensions/v2/mock-db.ts index 98d571b..1f4b9aa 100644 --- a/test/services/extensions/v2/mock-db.ts +++ b/test/services/extensions/v2/mock-db.ts @@ -1,12 +1,12 @@ type Row = Record; export interface MockTables { - authors: Map; + developers: Map; extensions: Map; extension_submissions: Map; - author_history: Map; - author_transfers: Map; - author_claims: Map; + developer_history: Map; + developer_transfers: Map; + developer_claims: Map; users: Map; // Test-only seam for simulating a write-through failure during approve(). forceExtensionWriteFailure?: boolean; @@ -14,12 +14,12 @@ export interface MockTables { export function createTables(): MockTables { return { - authors: new Map(), + developers: new Map(), extensions: new Map(), extension_submissions: new Map(), - author_history: new Map(), - author_transfers: new Map(), - author_claims: new Map(), + developer_history: new Map(), + developer_transfers: new Map(), + developer_claims: new Map(), users: new Map() }; } @@ -81,14 +81,14 @@ class MockStatement implements D1PreparedStatement { rows = rows.filter((r) => String(r.id).toLowerCase() === id); } return rows.map((r) => { - const author = this.tables.authors.get(String(r.author_id)); + const developer = this.tables.developers.get(String(r.author_id)); return { id: r.id, type: r.type, author_id: r.author_id, - author_type: author?.type ?? "user", - author_name: author?.name ?? "", - author_url: author?.url ?? null, + author_type: developer?.type ?? "user", + author_name: developer?.name ?? "", + author_url: developer?.url ?? null, name: r.name, description: r.description, releases: r.releases, @@ -115,31 +115,31 @@ class MockStatement implements D1PreparedStatement { return row ? [row] : []; } - if (q.startsWith("SELECT owner_user_id FROM authors WHERE id = ?")) { - const row = this.tables.authors.get(String(p[0])); + if (q.startsWith("SELECT owner_user_id FROM developers WHERE id = ?")) { + const row = this.tables.developers.get(String(p[0])); return row ? [row] : []; } - if (q.startsWith("SELECT * FROM authors WHERE owner_user_id = ?")) { - const row = [...this.tables.authors.values()].find( + if (q.startsWith("SELECT * FROM developers WHERE owner_user_id = ?")) { + const row = [...this.tables.developers.values()].find( (r) => r.owner_user_id === p[0] ); return row ? [row] : []; } - if (q.startsWith("SELECT * FROM authors WHERE id = ?")) { - const row = this.tables.authors.get(String(p[0])); + if (q.startsWith("SELECT * FROM developers WHERE id = ?")) { + const row = this.tables.developers.get(String(p[0])); return row ? [row] : []; } - if (q.startsWith("SELECT * FROM authors ORDER BY name")) { - return [...this.tables.authors.values()].sort((a, b) => + if (q.startsWith("SELECT * FROM developers ORDER BY name")) { + return [...this.tables.developers.values()].sort((a, b) => String(a.name ?? "").localeCompare(String(b.name ?? "")) ); } - if (q.startsWith("SELECT * FROM authors WHERE approved_at IS NULL")) { - return [...this.tables.authors.values()] + if (q.startsWith("SELECT * FROM developers WHERE approved_at IS NULL")) { + return [...this.tables.developers.values()] .filter((r) => (r.approved_at ?? null) === null) .sort((a, b) => String(a.created_at ?? "").localeCompare(String(b.created_at ?? "")) @@ -148,7 +148,7 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "INSERT INTO authors (id, type, name, url, bio, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at)" + "INSERT INTO developers (id, type, name, url, bio, avatar_url, contact_email, owner_user_id, approved_at, created_at, updated_at)" ) ) { const [ @@ -161,17 +161,17 @@ class MockStatement implements D1PreparedStatement { contact_email, owner_user_id ] = p; - // Mirrors idx_authors_owner_unique: one profile per non-null owner. - const ownerTaken = [...this.tables.authors.values()].some( + // Mirrors idx_developers_owner_unique: one profile per non-null owner. + const ownerTaken = [...this.tables.developers.values()].some( (r) => owner_user_id !== null && r.owner_user_id === owner_user_id ); if (ownerTaken) { throw new Error( - "D1_ERROR: UNIQUE constraint failed: authors.owner_user_id" + "D1_ERROR: UNIQUE constraint failed: developers.owner_user_id" ); } const now = new Date().toISOString(); - this.tables.authors.set(String(id), { + this.tables.developers.set(String(id), { id, type, name, @@ -189,11 +189,11 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE authors SET type = ?, name = ?, url = ?, bio = ?, avatar_url = ?, contact_email = ?, approved_at = NULL" + "UPDATE developers SET type = ?, name = ?, url = ?, bio = ?, avatar_url = ?, contact_email = ?, approved_at = NULL" ) ) { const [type, name, url, bio, avatar_url, contact_email, id] = p; - const row = this.tables.authors.get(String(id)); + const row = this.tables.developers.get(String(id)); if (row) { row.type = type; row.name = name; @@ -210,13 +210,13 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "INSERT INTO author_history (id, author_id, type, name, url, changed_by, changed_at)" + "INSERT INTO developer_history (id, developer_id, type, name, url, changed_by, changed_at)" ) ) { - const [id, author_id, type, name, url, changed_by] = p; - this.tables.author_history.set(String(id), { + const [id, developer_id, type, name, url, changed_by] = p; + this.tables.developer_history.set(String(id), { id, - author_id, + developer_id, type, name, url, @@ -228,14 +228,14 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "SELECT author_id, type, name, url, changed_by, changed_at FROM author_history WHERE author_id = ?" + "SELECT developer_id, type, name, url, changed_by, changed_at FROM developer_history WHERE developer_id = ?" ) ) { - const [author_id] = p; + const [developer_id] = p; // Newest-first, with ties (same-second CURRENT_TIMESTAMP) broken by // insertion order — mirrors the real query's `changed_at DESC, rowid DESC`. - return [...this.tables.author_history.values()] - .filter((r) => r.author_id === author_id) + return [...this.tables.developer_history.values()] + .filter((r) => r.developer_id === developer_id) .reverse() .sort((a, b) => String(b.changed_at).localeCompare(String(a.changed_at)) @@ -244,16 +244,16 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE author_transfers SET revoked_at = CURRENT_TIMESTAMP WHERE author_id = ? AND accepted_at IS NULL AND revoked_at IS NULL AND EXISTS" + "UPDATE developer_transfers SET revoked_at = CURRENT_TIMESTAMP WHERE developer_id = ? AND accepted_at IS NULL AND revoked_at IS NULL AND EXISTS" ) ) { - const [author_id, owner_user_id] = p; - const author = this.tables.authors.get(String(author_id)); + const [developer_id, owner_user_id] = p; + const developer = this.tables.developers.get(String(developer_id)); let changes = 0; - if (author?.owner_user_id === owner_user_id) { - for (const row of this.tables.author_transfers.values()) { + if (developer?.owner_user_id === owner_user_id) { + for (const row of this.tables.developer_transfers.values()) { if ( - row.author_id === author_id && + row.developer_id === developer_id && row.accepted_at === null && row.revoked_at === null ) { @@ -268,23 +268,23 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "INSERT INTO author_transfers (id, author_id, token_hash, created_by, expires_at) SELECT ?, ?, ?, ?, ? WHERE EXISTS" + "INSERT INTO developer_transfers (id, developer_id, token_hash, created_by, expires_at) SELECT ?, ?, ?, ?, ? WHERE EXISTS" ) ) { const [ id, - author_id, + developer_id, token_hash, created_by, expires_at, - checkAuthorId, + checkDeveloperId, owner_user_id ] = p; - const author = this.tables.authors.get(String(checkAuthorId)); - if (author?.owner_user_id === owner_user_id) { - this.tables.author_transfers.set(String(id), { + const developer = this.tables.developers.get(String(checkDeveloperId)); + if (developer?.owner_user_id === owner_user_id) { + this.tables.developer_transfers.set(String(id), { id, - author_id, + developer_id, token_hash, created_by, created_at: new Date().toISOString(), @@ -302,15 +302,15 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE author_transfers SET accepted_at = CURRENT_TIMESTAMP, accepted_by = ? WHERE token_hash = ?" + "UPDATE developer_transfers SET accepted_at = CURRENT_TIMESTAMP, accepted_by = ? WHERE token_hash = ?" ) ) { const [accepted_by, token_hash, owner_user_id] = p; const now = new Date(); - const ownsAny = [...this.tables.authors.values()].some( + const ownsAny = [...this.tables.developers.values()].some( (r) => r.owner_user_id === owner_user_id ); - const row = [...this.tables.author_transfers.values()].find( + const row = [...this.tables.developer_transfers.values()].find( (r) => r.token_hash === token_hash && r.accepted_at === null && @@ -329,12 +329,12 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "SELECT 1 FROM author_transfers WHERE token_hash = ? AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > CURRENT_TIMESTAMP" + "SELECT 1 FROM developer_transfers WHERE token_hash = ? AND accepted_at IS NULL AND revoked_at IS NULL AND expires_at > CURRENT_TIMESTAMP" ) ) { const [token_hash] = p; const now = new Date(); - const row = [...this.tables.author_transfers.values()].find( + const row = [...this.tables.developer_transfers.values()].find( (r) => r.token_hash === token_hash && r.accepted_at === null && @@ -346,30 +346,30 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "SELECT author_id FROM author_transfers WHERE token_hash = ?" + "SELECT developer_id FROM developer_transfers WHERE token_hash = ?" ) ) { const [token_hash] = p; - const row = [...this.tables.author_transfers.values()].find( + const row = [...this.tables.developer_transfers.values()].find( (r) => r.token_hash === token_hash ); - return row ? [{ author_id: row.author_id }] : []; + return row ? [{ developer_id: row.developer_id }] : []; } if ( q.startsWith( - "UPDATE authors SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE changes() = 1 AND id = ( SELECT author_id FROM author_transfers" + "UPDATE developers SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE changes() = 1 AND id = ( SELECT developer_id FROM developer_transfers" ) ) { const [owner_user_id, token_hash, accepted_by] = p; - const transfer = [...this.tables.author_transfers.values()].find( + const transfer = [...this.tables.developer_transfers.values()].find( (r) => r.token_hash === token_hash && r.accepted_by === accepted_by && r.accepted_at !== null ); const row = transfer - ? this.tables.authors.get(String(transfer.author_id)) + ? this.tables.developers.get(String(transfer.developer_id)) : undefined; if (row) { row.owner_user_id = owner_user_id; @@ -384,11 +384,11 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE authors SET approved_at = CURRENT_TIMESTAMP WHERE id = ?" + "UPDATE developers SET approved_at = CURRENT_TIMESTAMP WHERE id = ?" ) ) { const [id] = p; - const row = this.tables.authors.get(String(id)); + const row = this.tables.developers.get(String(id)); if (row) { row.approved_at = new Date().toISOString(); this.changes = 1; @@ -396,8 +396,8 @@ class MockStatement implements D1PreparedStatement { return []; } - if (q.startsWith("SELECT 1 FROM authors WHERE owner_user_id = ?")) { - const row = [...this.tables.authors.values()].find( + if (q.startsWith("SELECT 1 FROM developers WHERE owner_user_id = ?")) { + const row = [...this.tables.developers.values()].find( (r) => r.owner_user_id === p[0] ); return row ? [{ "1": 1 }] : []; @@ -405,35 +405,41 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "INSERT INTO author_claims (id, author_id, claimant_id, note) SELECT ?, ?, ?, ? WHERE EXISTS" + "INSERT INTO developer_claims (id, developer_id, claimant_id, note) SELECT ?, ?, ?, ? WHERE EXISTS" ) ) { - const [id, author_id, claimant_id, note, checkAuthorId, checkClaimantId] = - p; - const author = this.tables.authors.get(String(checkAuthorId)); - const claimantOwnsAny = [...this.tables.authors.values()].some( + const [ + id, + developer_id, + claimant_id, + note, + checkDeveloperId, + checkClaimantId + ] = p; + const developer = this.tables.developers.get(String(checkDeveloperId)); + const claimantOwnsAny = [...this.tables.developers.values()].some( (r) => r.owner_user_id === checkClaimantId ); - if (!author || author.owner_user_id !== null || claimantOwnsAny) { + if (!developer || developer.owner_user_id !== null || claimantOwnsAny) { this.changes = 0; return []; } - const pendingExists = [...this.tables.author_claims.values()].some( + const pendingExists = [...this.tables.developer_claims.values()].some( (r) => - r.author_id === author_id && + r.developer_id === developer_id && r.claimant_id === claimant_id && r.status === "pending" ); if (pendingExists) { throw new Error( - "D1_ERROR: UNIQUE constraint failed: author_claims.author_id, author_claims.claimant_id" + "D1_ERROR: UNIQUE constraint failed: developer_claims.developer_id, developer_claims.claimant_id" ); } const now = new Date().toISOString(); - this.tables.author_claims.set(String(id), { + this.tables.developer_claims.set(String(id), { id, - author_id, + developer_id, claimant_id, status: "pending", note, @@ -446,17 +452,17 @@ class MockStatement implements D1PreparedStatement { return []; } - if (q.startsWith("SELECT * FROM author_claims WHERE id = ?")) { - const row = this.tables.author_claims.get(String(p[0])); + if (q.startsWith("SELECT * FROM developer_claims WHERE id = ?")) { + const row = this.tables.developer_claims.get(String(p[0])); return row ? [row] : []; } if ( q.startsWith( - "SELECT * FROM author_claims WHERE claimant_id = ? ORDER BY created_at DESC" + "SELECT * FROM developer_claims WHERE claimant_id = ? ORDER BY created_at DESC" ) ) { - return [...this.tables.author_claims.values()] + return [...this.tables.developer_claims.values()] .filter((r) => r.claimant_id === p[0]) .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)) @@ -464,30 +470,32 @@ class MockStatement implements D1PreparedStatement { } if ( - q.startsWith("SELECT c.*, a.name AS author_name, a.type AS author_type") + q.startsWith( + "SELECT c.*, d.name AS developer_name, d.type AS developer_type" + ) ) { - return [...this.tables.author_claims.values()] + return [...this.tables.developer_claims.values()] .filter((r) => r.status === "pending") .sort((a, b) => String(a.created_at).localeCompare(String(b.created_at)) ) .map((r) => { - const author = this.tables.authors.get(String(r.author_id)); + const developer = this.tables.developers.get(String(r.developer_id)); return { ...r, - author_name: author?.name ?? "", - author_type: author?.type ?? "user" + developer_name: developer?.name ?? "", + developer_type: developer?.type ?? "user" }; }); } if ( q.startsWith( - "UPDATE author_claims SET status = 'approved', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'" + "UPDATE developer_claims SET status = 'approved', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'" ) ) { const [reviewer_id, id] = p; - const row = this.tables.author_claims.get(String(id)); + const row = this.tables.developer_claims.get(String(id)); if (row && row.status === "pending") { row.status = "approved"; row.reviewer_id = reviewer_id; @@ -501,11 +509,11 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE authors SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND owner_user_id IS NULL" + "UPDATE developers SET owner_user_id = ?, approved_at = NULL, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND owner_user_id IS NULL" ) ) { const [owner_user_id, id] = p; - const row = this.tables.authors.get(String(id)); + const row = this.tables.developers.get(String(id)); if (row && row.owner_user_id === null) { row.owner_user_id = owner_user_id; row.approved_at = null; @@ -519,14 +527,14 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE author_claims SET status = 'rejected', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP, review_note = 'Another claim on this profile was approved' WHERE changes() = 1 AND author_id = ?" + "UPDATE developer_claims SET status = 'rejected', reviewer_id = ?, reviewed_at = CURRENT_TIMESTAMP, review_note = 'Another claim on this profile was approved' WHERE changes() = 1 AND developer_id = ?" ) ) { - const [reviewer_id, author_id, excludeId] = p; + const [reviewer_id, developer_id, excludeId] = p; const now = new Date().toISOString(); - for (const row of this.tables.author_claims.values()) { + for (const row of this.tables.developer_claims.values()) { if ( - row.author_id === author_id && + row.developer_id === developer_id && row.status === "pending" && row.id !== excludeId ) { @@ -541,11 +549,11 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE author_claims SET status = 'pending', reviewer_id = NULL, reviewed_at = NULL WHERE id = ?" + "UPDATE developer_claims SET status = 'pending', reviewer_id = NULL, reviewed_at = NULL WHERE id = ?" ) ) { const [id] = p; - const row = this.tables.author_claims.get(String(id)); + const row = this.tables.developer_claims.get(String(id)); if (row) { row.status = "pending"; row.reviewer_id = null; @@ -556,11 +564,11 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "UPDATE author_claims SET status = 'rejected', reviewer_id = ?, review_note = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'" + "UPDATE developer_claims SET status = 'rejected', reviewer_id = ?, review_note = ?, reviewed_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'pending'" ) ) { const [reviewer_id, review_note, id] = p; - const row = this.tables.author_claims.get(String(id)); + const row = this.tables.developer_claims.get(String(id)); if (row && row.status === "pending") { row.status = "rejected"; row.reviewer_id = reviewer_id; @@ -580,14 +588,14 @@ class MockStatement implements D1PreparedStatement { if ( q.startsWith( - "INSERT INTO extension_submissions (id, extension_id, author_id, submitted_by, status, payload)" + "INSERT INTO extension_submissions (id, extension_id, developer_id, submitted_by, status, payload)" ) ) { - const [id, extension_id, author_id, submitted_by, payload] = p; + const [id, extension_id, developer_id, submitted_by, payload] = p; this.tables.extension_submissions.set(String(id), { id, extension_id, - author_id, + developer_id, submitted_by, status: "pending", payload, @@ -661,11 +669,13 @@ class MockStatement implements D1PreparedStatement { } if ( - q.startsWith("INSERT INTO authors (id, type, name, url, owner_user_id)") + q.startsWith( + "INSERT INTO developers (id, type, name, url, owner_user_id)" + ) ) { const [id, type, name, url, owner_user_id] = p; - const existing = this.tables.authors.get(String(id)); - this.tables.authors.set(String(id), { + const existing = this.tables.developers.get(String(id)); + this.tables.developers.set(String(id), { id, type, name,