fix(website): publish failed with uuid type error on Better-Auth ids

published_by, created_by, and space_id were declared as uuid, but
Mana user + space ids are Better-Auth nanoids stored as text. The
insert into website.published_snapshots raised `invalid input syntax
for type uuid` and Hono swallowed it as a generic 500.

Changes:
- schema.ts: uuid -> text on the three columns
- 0003_fix_id_types.sql: ALTER COLUMN on existing installs
- publish.ts: replace UUID regex on X-Mana-Space with a nanoid-shaped
  check (it was silently nulling valid space ids)
- publish.ts: log + return the actual error message on the 500 path
  so the next unhandled failure is visible instead of opaque

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-23 21:43:57 +02:00
parent 645993db01
commit c404db5b6e
3 changed files with 31 additions and 6 deletions

View file

@ -0,0 +1,18 @@
-- Website module — fix id column types.
--
-- `published_by`, `created_by`, and `space_id` were originally declared
-- as `uuid`, but Mana user ids (auth.users.id) and space ids
-- (auth.organizations.id) are Better-Auth nanoids stored as `text`.
-- Publishing failed with `invalid input syntax for type uuid`.
--
-- Apply with:
-- psql "$DATABASE_URL" -f apps/api/drizzle/website/0003_fix_id_types.sql
ALTER TABLE "website"."published_snapshots"
ALTER COLUMN "published_by" TYPE text;
ALTER TABLE "website"."published_snapshots"
ALTER COLUMN "space_id" TYPE text;
ALTER TABLE "website"."custom_domains"
ALTER COLUMN "created_by" TYPE text;

View file

@ -70,7 +70,10 @@ routes.post('/sites/:id/publish', async (c) => {
// embed the active space in JWT claims). Nullable — full membership
// check lands in M6; M2 stores whatever the client declares.
const spaceIdHeader = c.req.header('X-Mana-Space');
const spaceId = spaceIdHeader && /^[0-9a-f-]{36}$/i.test(spaceIdHeader) ? spaceIdHeader : null;
// Better-Auth nanoid format: 32 URL-safe base64 chars. Keep this lax
// (no hard length) so it doesn't drift if nanoid config changes.
const spaceId =
spaceIdHeader && /^[A-Za-z0-9_-]{16,64}$/.test(spaceIdHeader) ? spaceIdHeader : null;
const siteId = c.req.param('id');
if (!siteId) {
@ -177,7 +180,10 @@ routes.post('/sites/:id/publish', async (c) => {
}
websitePublishTotal.inc({ result: 'error' });
publishTimer();
throw err;
console.error('[website/publish] unhandled error', err);
return errorResponse(c, err instanceof Error ? err.message : 'Publish failed', 500, {
code: 'PUBLISH_FAILED',
});
}
});

View file

@ -45,15 +45,15 @@ export const publishedSnapshots = websiteSchema.table(
/** True for the row served to the public. Exactly one per slug. */
isCurrent: boolean('is_current').notNull().default(false),
publishedAt: timestamp('published_at', { withTimezone: true }).defaultNow().notNull(),
/** User who pressed the publish button. */
publishedBy: uuid('published_by').notNull(),
/** User who pressed the publish button. Better-Auth nanoid, not UUID. */
publishedBy: text('published_by').notNull(),
/**
* Space the site belongs to. Nullable in M2 because mana-auth
* doesn't yet thread the active space into JWT claims the
* client can pass it via `X-Mana-Space`, but we don't hard-require
* it until server-side membership check lands (M6).
*/
spaceId: uuid('space_id'),
spaceId: text('space_id'),
},
(table) => [
index('published_snapshots_site_idx').on(table.siteId, table.publishedAt),
@ -121,7 +121,8 @@ export const customDomains = websiteSchema.table(
verifiedAt: timestamp('verified_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
createdBy: uuid('created_by').notNull(),
/** Better-Auth nanoid, not UUID. */
createdBy: text('created_by').notNull(),
},
(table) => [index('custom_domains_site_idx').on(table.siteId, table.status)]
// A partial unique index on (hostname) WHERE status='verified' lives