Merge main into monorepo-bootstrap (231 commits)

Reconcile main with the package-extraction branch. 8 conflicts resolved by
keeping the @civitai/* package shims and porting main's NEW logic into the
packages (not the app shims):

- prisma enums/models: re-export shims; regenerated from merged schema (Model3D*)
- package.json/pnpm-lock: union scripts; @civitai/client beta.71->73, +three
- @civitai/axiom: logToAxiom stderr-before-guards reordering (test relocated here)
- @civitai/telemetry: redisSelfHealReconnect + redisMetricWriteFailSoft counters
- clickhouse tracker.ts: Tracker.view() context fields + new blockRender()
- @civitai/redis: ported main's cluster self-heal + packed-compression subsystem
  (cluster-selfheal/inflight/deadline-hits/packed-compression moved into the
  package, deadline.ts + 8 REDIS_CLUSTER_SELFHEAL_* env vars wired, metric bridge)
- search services: dropped next-auth import -> ~/types/session SessionUser

Verified: typecheck 0 errors (app+packages); redis(53)/axiom(3)/app redis+logging(78)
tests pass; two independent review agents confirmed no lost main functionality and a
faithful (9/9 invariants) redis-resilience port.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
briant
2026-06-22 15:23:28 -06:00
372 changed files with 38219 additions and 1911 deletions
+83 -16
View File
@@ -32,9 +32,22 @@ import { trpcQuery } from './preview-trpc';
* projection) + `blockId` (the `<slug>` used by the page route).
* - Page route `/apps/run/[slug]/[[...path]]` SSR-gates on
* `features.appBlocks && features.appBlocksPages`, resolves the approved app
* by `block_id`, and renders `<PageBlockHost>` (data-testid="app-page-frame"
* with the `<iframe data-testid="app-page-iframe">` inside, plus the W7
* `data-testid="app-block-chrome"` trust bar).
* by `block_id`, and renders `<PageBlockHost>`: the W7 trust chrome
* (`AppBlockChrome`, with an "App block menu" button) above the block
* `<iframe>` (`title=<appName>`, `data-block-instance-id="page_<appBlockId>"`,
* `data-block-ready` flipping to "true" on BLOCK_READY).
*
* ⚠️ SELECTORS — NO `data-testid`. The preview is a PRODUCTION Next build
* (NODE_ENV=production), and next.config.mjs strips every `data-testid` in
* production (`compiler.reactRemoveProperties: { properties: ['^data-testid$'] }`).
* So `getByTestId('app-page-iframe' | 'app-block-chrome' | 'app-page-frame')`
* NEVER matches against a deployed preview — the elements render fine, the
* attribute is just gone. We therefore assert on attributes the production build
* KEEPS: the chrome's accessible "App block menu" button, and the iframe's
* `data-block-instance-id` (a `page_*` id) + `data-block-ready`. (`reactRemove
* Properties` only strips `^data-testid$`, so other `data-*` attrs survive.)
* This mirrors the sibling preview-apps-* specs, which assert via tRPC + ARIA
* roles, never rendered testids.
*/
const ROLE = 'mod' as const;
@@ -52,7 +65,7 @@ type ListAvailableResult = { items: AvailableBlock[]; nextCursor?: string };
test.describe('App Blocks full-page app surface (mod)', () => {
test.use({ storageState: storageStatePath(ROLE) });
test('a page-declaring app opens at /apps/run/<slug> and the iframe mounts + inits', async ({
test('a page-declaring app opens at /apps/run/<slug>: chrome + iframe mount and the host mints a page token', async ({
page,
}) => {
// DISCOVER a page-declaring approved app from the public listing. `{}` is a
@@ -79,23 +92,77 @@ test.describe('App Blocks full-page app surface (mod)', () => {
).toBeLessThan(400);
// The host trust chrome (rendered in civitai-web, spoof-proof) is present —
// proves PageBlockHost mounted (not a 404 / blank).
// proves PageBlockHost mounted (not a 404 / blank). The chrome's "App block
// menu" button is unique to AppBlockChrome and uses a production-safe
// accessible name (NOT a stripped data-testid).
await expect(
page.getByTestId('app-block-chrome'),
page.getByRole('button', { name: 'App block menu' }),
'the full-page host should render the W7 trust chrome'
).toBeVisible();
// The block iframe mounts (server-resolved manifest.iframe.src).
const frame = page.getByTestId('app-page-iframe');
// The block iframe mounts (server-resolved manifest.iframe.src). Located by
// its surviving `data-block-instance-id` (a synthetic `page_<appBlockId>` id
// unique to the page host) — `getByTestId` would never match (stripped).
const frame = page.locator('iframe[data-block-instance-id^="page_"]');
await expect(frame, 'the full-page block iframe should mount').toBeVisible();
// The host posts BLOCK_INIT (viewer page token + subPath) on a retry loop
// until the block acks BLOCK_READY → data-block-ready flips to "true". This
// is the e2e proof that the page-mint + handshake worked. Allow generous
// time for the cross-origin bundle to load + ack on a cold preview.
await expect(
frame,
'the full-page block should receive BLOCK_INIT and ack ready (page token minted)'
).toHaveAttribute('data-block-ready', 'true', { timeout: 20_000 });
// The iframe points at the server-resolved block origin (manifest.iframe.src
// → `<slug>.civit.ai`), not a blank/about:blank — proves the SSR-resolved
// src reached the DOM.
const iframeSrc = await frame.getAttribute('src');
expect(iframeSrc, 'the iframe src is the server-resolved block origin').toMatch(
/^https?:\/\//
);
// PAGE-MINT PROOF (the host-side half of the handshake we CAN verify on a
// preview): the host posts BLOCK_INIT only after minting a viewer-scoped page
// token from POST /api/v1/block-tokens (entityType:'none', `page_<appBlockId>`).
// Exercise that exact mint with the mod cookie — a 200 + non-empty token is
// the e2e proof the page-mint path works (two-flag gate cleared, synthetic
// page instance resolved, JWT issued). The host then posts BLOCK_INIT to the
// block origin.
//
// We do NOT assert `data-block-ready === 'true'` (the block's BLOCK_READY
// ack): that flips only when the cross-origin block bundle at `<slug>.civit.ai`
// ACCEPTS the BLOCK_INIT, which requires the PARENT origin (here the ephemeral
// preview host `pr-N.civitaic.com`) to be in the block's own
// `allowedParentOrigins` allowlist. Production blocks allowlist civitai.com /
// *.civit.ai, never an ephemeral preview origin, so the ack can never arrive
// on a preview no matter how correct civitai-web is — asserting it makes this
// spec un-passable by construction. The host-side contract (chrome + iframe +
// page-token mint) is what a preview can prove; the BLOCK_READY round-trip is
// covered by the PageBlockHost.browser.test.tsx component test with a stubbed
// block.
const tokenResp = await page.request.post('/api/v1/block-tokens', {
headers: {
'content-type': 'application/json',
origin: process.env.PREVIEW_URL ?? '',
referer: `${process.env.PREVIEW_URL ?? ''}/apps/run/${pageApp.blockId}`,
},
data: {
// Synthetic page instance id: `page_<appBlockId>`, where appBlockId is
// `AvailableBlock.id` (== app_blocks.id, the `apb_*` value) — the same id
// the page route builds in `page_${appBlockId}`.
blockInstanceId: `page_${pageApp.id}`,
slotContext: {
slotId: 'app.page',
entityType: 'none',
slug: pageApp.blockId,
subPath: '',
viewerUserId: null,
viewerUsername: null,
theme: 'dark',
},
},
});
expect(
tokenResp.status(),
'the host mints a viewer-scoped page token (the BLOCK_INIT prerequisite)'
).toBe(200);
const tokenBody = (await tokenResp.json()) as { token?: string };
expect(
typeof tokenBody.token === 'string' && tokenBody.token.length > 0,
'page-token mint returns a non-empty JWT'
).toBe(true);
});
});
+72
View File
@@ -43,6 +43,7 @@ const FEATURE_FLAGS_PROCEDURE = 'user.getFeatureFlags';
const TOS_PROCEDURE = 'content.checkTosUpdate';
const ANNOUNCEMENTS_PROCEDURE = 'announcement.getAnnouncements';
const FOLLOWING_PROCEDURE = 'user.getFollowingUsers';
const LIVE_NOW_PROCEDURE = 'system.getLiveNow';
// A core page a gate-passing user lands on directly (no /login bounce). Both
// procedures fire on every logged-in bootstrap regardless of which page, so
@@ -399,6 +400,77 @@ test.describe('SSR-injected getFollowingUsers (logged-in)', () => {
});
});
/**
* Regression guard for the SSR-inject of `system.getLiveNow` (~26/s on
* api-primary). NOT auth-gated — it is a `publicProcedure` consumed by the
* ambient `useIsLive` hook (header logo, social links, social home block), so
* it fires on every bootstrap, anon and authed alike. SSR-computed in
* `_app.getInitialProps` (fail-open to `false`) and seeded in AppProvider on the
* fixed `undefined` key. The payload is a plain `boolean` — no Date/array
* serialization subtlety, so the seed (JSON) and a live fetch (`result.data.json`)
* compare directly. The seed can legitimately be `false` (stream offline); that
* is still a valid, present seed and the byte-equality assertion holds.
*/
test.describe('SSR-injected getLiveNow (logged-in)', () => {
test.use({ storageState: storageStatePath('mod') });
test('getLiveNow is seeded into __NEXT_DATA__ and not fetched on bootstrap', async ({ page }) => {
const trpcUrls = await collectTrpcRequests(page, AUTHED_LANDING);
const liveNow = await readPageProp(page, 'liveNow');
expect(liveNow.present, 'liveNow present in __NEXT_DATA__ pageProps').toBe(true);
expect(typeof liveNow.value, 'liveNow seed is a boolean').toBe('boolean');
const liveNowRequests = trpcUrls.filter((u) => u.includes(LIVE_NOW_PROCEDURE));
expect(
liveNowRequests,
`no ${LIVE_NOW_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${liveNowRequests.join(
'\n'
)}`
).toHaveLength(0);
});
test('SSR seed byte-equals a live fetch', async ({ page }) => {
await page.goto(AUTHED_LANDING, { waitUntil: 'domcontentloaded' });
const seed = await readPageProp(page, 'liveNow');
expect(seed.present, 'liveNow seed present in __NEXT_DATA__').toBe(true);
const live = await fetchTrpcQueryJson(page, LIVE_NOW_PROCEDURE);
expect(
seed.value,
'system.getLiveNow SSR seed must byte-equal a live resolver fetch'
).toEqual(live);
expect(typeof live, 'live getLiveNow is a boolean').toBe('boolean');
});
});
test.describe('SSR-injected getLiveNow (anonymous)', () => {
// `system.getLiveNow` is a publicProcedure and fires for anon too, so the seed
// must be present on the anon bootstrap as well. No anon "byte-equals a live
// fetch" test: the preview-auth gate blocks ALL anonymous /api/trpc/* and
// 302-redirects to /login (same reason documented for the anon announcements
// case), so an anon live fetch never returns tRPC JSON. The seed-present +
// no-bootstrap-fetch contract is what matters and is asserted here.
test.use({ storageState: { cookies: [], origins: [] } });
test('getLiveNow is seeded into __NEXT_DATA__ and not fetched on bootstrap', async ({ page }) => {
const trpcUrls = await collectTrpcRequests(page, ANON_LANDING);
const liveNow = await readPageProp(page, 'liveNow');
expect(liveNow.present, 'liveNow present in __NEXT_DATA__ pageProps').toBe(true);
expect(typeof liveNow.value, 'liveNow seed is a boolean').toBe('boolean');
const liveNowRequests = trpcUrls.filter((u) => u.includes(LIVE_NOW_PROCEDURE));
expect(
liveNowRequests,
`no ${LIVE_NOW_PROCEDURE} tRPC request should fire on bootstrap (it is SSR-injected); saw:\n${liveNowRequests.join(
'\n'
)}`
).toHaveLength(0);
});
});
/**
* MANUAL CHECKLIST — behaviours of this PR that are not auto-asserted above.
* (Auth IS supported by this harness, but these are timing/visual/state cases