Files
civitai__civitai/package.json
T

439 lines
18 KiB
JSON
Raw Normal View History

2022-10-11 16:56:51 -04:00
{
"name": "model-share",
2026-08-07 22:54:11 -06:00
"version": "5.0.2254",
2022-10-11 16:56:51 -04:00
"private": true,
"packageManager": "pnpm@10.28.1",
chore: align Node version across engines, nvmrc, Dockerfile and types (#3365) * chore: align Node version across engines, nvmrc, Dockerfile and types .nvmrc was left at 24.18.0 when #3311 rolled the runtime base image back from node:24-alpine3.22 to node:22-alpine3.20 over the V8 13.6 server-CPU regression. The Dockerfile is the shipping truth, so everything else moves to 22: - .nvmrc: 24.18.0 -> 22 (bare major mirrors the Dockerfile's floating node:22 tag; an exact patch here would re-drift on every 22.x release) - package.json: add engines.node ">=22.0.0" (Node 20 is EOL as of 2026-04-30; floor, not a ceiling) - @types/node: 20.19.9 -> 22.20.1, matching the runtime Typecheck is clean; the @types/node bump surfaced no errors. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore: pin .nvmrc to the image's exact Node, bump nix + devcontainer off 20 node:22-alpine3.20 is frozen at Node 22.16.0 (last pushed 2025-05-21; Alpine 3.20 went EOL 2026-04-01 and the tag is no longer rebuilt). A bare "22" in .nvmrc resolves to 22.23.1 via actions/setup-node, so #3362's CI would have typechecked seven minors ahead of prod. 22.16.0 is the value #3242 replaced and the only one that matches the shipping image. Two more Node 20 environments moved off the EOL line (neither is a production surface): - flake.nix: nodejs_20 -> nodejs_22 (devShell; would have warned on every pnpm install against the new engines floor) - .devcontainer/public: typescript-node:1-20 -> 1-22 (outside-contributor onboarding path) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 16:53:43 -06:00
"engines": {
"node": ">=22.0.0"
},
2022-10-11 16:56:51 -04:00
"scripts": {
"preinstall": "npx only-allow pnpm",
2022-12-02 22:58:15 -07:00
"start": "next start",
2024-09-06 18:29:14 -04:00
"start-debug": "NODE_OPTIONS='--inspect' next start",
"build:workers": "node scripts/build-workers.mjs",
"clean": "node -e \"fs.rmSync('.next',{recursive:true,force:true})\"",
"predev": "pnpm build:workers",
2022-10-11 16:56:51 -04:00
"dev": "next dev",
"dev:auth": "pnpm --filter @civitai/auth-app dev",
"dev:moderator": "pnpm --filter @civitai/moderator-app dev",
"dev:creator-studio": "pnpm --filter @civitai/creator-studio-app dev",
"dev:storage": "pnpm --filter @civitai/storage-app dev",
"dev-low": "cross-env NODE_OPTIONS=\"--max_old_space_size=6144\" next dev",
"dev-debug": "pnpm build:workers && cross-env NODE_OPTIONS=\"--max_old_space_size=8192 --inspect\" next dev",
"dev-snap": "cross-env NODE_OPTIONS=\"--max_old_space_size=8192 --heapsnapshot-near-heap-limit=3\" next dev",
"dev:daemon": "node .claude/skills/dev-server/console.mjs",
"dev:rgb": "node .claude/skills/dev-server/cli.mjs rgb start",
"dev:rgb:stop": "node .claude/skills/dev-server/cli.mjs rgb stop",
"dev:rgb:status": "node .claude/skills/dev-server/cli.mjs rgb status",
2024-11-04 15:52:10 -05:00
"prod": "cross-env NODE_ENV=production next dev",
2023-04-12 20:34:56 +01:00
"boost": "next-boost",
"release:base": "git checkout release && git pull --rebase && git rebase main && git push --force-with-lease && git checkout main",
"release:major": "git pull && npm version major && git push --follow-tags && pnpm run release:base",
"release:minor": "git pull && npm version minor && git push --follow-tags && pnpm run release:base",
"release:patch": "git pull && npm version patch && git push --follow-tags && pnpm run release:base",
"release": "pnpm run release:patch",
chore(auth): add release:auth scripts + sync version; remove redundant GH Actions build (#2781) * chore(auth): add release:auth scripts + sync version; remove redundant GH Actions build Wire a per-app release flow for the auth hub (apps/auth), mirroring the main app's `pnpm run release` ergonomics: - Add root `release:auth[:patch|:minor|:major]` scripts. They bump apps/auth/package.json via `npm --prefix apps/auth version <bump> --tag-version-prefix=auth-app-v`, create an `auth-app-vX.Y.Z` tag, and push --follow-tags. No `release:base` — the hub deploys off the tag (ghcr+Flux), not the `release` branch. - Sync apps/auth/package.json 0.0.0 -> 0.1.0 to match the manually-cut deployed 0.1.0, so the first scripted release is 0.1.1 (above the deployed semver that Flux's highest-semver ImagePolicy selects). - Remove .github/workflows/auth-app.yml: the hub now builds in-cluster via the Tekton tag-webhook on the same auth-app-v* tags; keeping the GH Actions workflow would double-build (paid runner) and push a competing image. - Docs: add docs/auth/releasing.md, link it from auth-index.md, and add a Releasing section to apps/auth/README.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(auth-release): commit+tag via release-app.mjs (npm --prefix skips git at root) Audit (PR #2781) found the release:auth scripts were inert: npm version --prefix apps/auth only creates the commit/tag when .git is in apps/auth, but .git is at the monorepo root — so it silently bumped the version field and skipped the tag (exit 0), releasing nothing + leaving a dirty tree. Replace with scripts/release-app.mjs: guards on-main + clean-tree, git pull --rebase, bumps with --no-git-tag-version, then commits ONLY apps/auth/package.json + annotated tag + push --follow-tags. Verified in a throwaway monorepo (tag created+pushed, root untouched, tree clean). Docs: prerequisites + rollback + one-at-a-time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 17:00:15 -05:00
"release:auth": "pnpm run release:auth:patch",
"release:auth:patch": "node scripts/release-app.mjs apps/auth auth-app-v patch",
"release:auth:minor": "node scripts/release-app.mjs apps/auth auth-app-v minor",
"release:auth:major": "node scripts/release-app.mjs apps/auth auth-app-v major",
fix(notifications): @prisma/client ESM interop (app-scoped) + wire release:notifications (#2897) * fix(db-schema): default-import @prisma/client for ESM-bundle interop `export { Prisma, PrismaClient } from '@prisma/client'` crashes at boot in the strict-ESM app bundles (esbuild format:esm — apps/notifications, and any @civitai/db consumer): @prisma/client's import condition resolves to a CJS file whose exports Node's cjs-module-lexer can't enumerate, so the named import throws "Named export 'Prisma' not found". Default-import the module.exports object and destructure the runtime values instead (cast to the module namespace type to keep full types). Works in both webpack/CJS (monolith) and esbuild/ESM (spun-out apps). Verified by a clean local boot of apps/notifications after the change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(notifications): wire up pnpm release:notifications scripts Mirror the auth service's release wiring so devs cut a notifications release with one command (like release:auth): pnpm release:notifications # patch pnpm release:notifications:minor pnpm release:notifications:major Each runs scripts/release-app.mjs to bump apps/notifications/package.json, tag notifications-v<semver> (matches the Tekton tag-webhook APP_CONFIG 'notifications' prefix), and push — the webhook builds + Flux deploys. Set apps/notifications version to 0.0.4 (the current built image) so the next patch cuts 0.0.5. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(notifications/Dockerfile): carry the generated Prisma client into the runtime `pnpm deploy --prod` resolves only lockfile packages, so it dropped the generated Prisma client (produced by the deps-stage db:generate into the pnpm virtual store, not a lockfile package). @prisma/client/default.js require('.prisma/client/default') at import (pulled in via @civitai/db -> @civitai/db-schema), so the runtime crashlooped 'Cannot find module .prisma/client/default'. Copy the generated client from node_modules/.pnpm/@prisma+client@*/node_modules/.prisma into the flattened deploy tree. Verified: docker build + run of this exact image boots clean ('notifications listening on 0.0.0.0:3000'). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(notifications): handle @prisma/client ESM interop in the app bundle (not db-schema) Revert the shared @civitai/db-schema change (it broke the monolith typecheck — you can't re-export the Prisma NAMESPACE as both an ESM-safe runtime value and a TS type from one barrel) and instead fix the interop app-locally: a tsup alias redirects @prisma/client to prisma-client.shim.mjs, which createRequire's the REAL client (CJS module.exports, always safe) and re-exports { Prisma, PrismaClient } as ESM. Keeps real Prisma at runtime (@civitai/db uses Prisma.sql / new PrismaClient), and the monolith is untouched (it builds with webpack where the named re-export is fine). Verified: docker build + run of the production image boots clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 15:09:28 -05:00
"release:notifications": "pnpm run release:notifications:patch",
"release:notifications:patch": "node scripts/release-app.mjs apps/notifications notifications-v patch",
"release:notifications:minor": "node scripts/release-app.mjs apps/notifications notifications-v minor",
"release:notifications:major": "node scripts/release-app.mjs apps/notifications notifications-v major",
"release:creator-studio": "pnpm run release:creator-studio:patch",
"release:creator-studio:patch": "node scripts/release-app.mjs apps/creator-studio creator-studio-v patch",
"release:creator-studio:minor": "node scripts/release-app.mjs apps/creator-studio creator-studio-v minor",
"release:creator-studio:major": "node scripts/release-app.mjs apps/creator-studio creator-studio-v major",
"release:storage": "pnpm run release:storage:patch",
"release:storage:patch": "node scripts/release-app.mjs apps/storage storage-v patch",
"release:storage:minor": "node scripts/release-app.mjs apps/storage storage-v minor",
"release:storage:major": "node scripts/release-app.mjs apps/storage storage-v major",
"release:event-engine": "pnpm run release:event-engine:patch",
"release:event-engine:patch": "node scripts/release-app.mjs apps/event-engine event-engine-v patch",
"release:event-engine:minor": "node scripts/release-app.mjs apps/event-engine event-engine-v minor",
"release:event-engine:major": "node scripts/release-app.mjs apps/event-engine event-engine-v major",
2026-08-04 14:15:26 -06:00
"release:moderator": "pnpm run release:moderator:patch",
build(moderator): add the Dockerfile + release scripts so apps/moderator can ship (#3582) apps/moderator is the only app under apps/ without a Dockerfile or a release:<app> script, so there is currently no way to produce an image for it — the in-cluster tag-webhook builds `apps/<name>/Dockerfile` on a `<prefix>-vX.Y.Z` tag, and nothing cuts that tag. The Dockerfile mirrors apps/creator-studio's proven four-stage shape (both are SvelteKit + adapter-node off the pnpm workspace): a filtered frozen install, a build stage, a pruned `pnpm deploy` for the runtime node_modules, and a non-root runtime on :3000. The build stage sets throwaway DATABASE_URL / DATABASE_REPLICA_URL because src/lib/server/db.ts calls createKyselyClients with required() on both at module load, and SvelteKit's postbuild `analyse` step imports the server modules. They live only in that stage and never reach the runtime image. Redis, ClickHouse and the orchestrator client are all lazily constructed, so they need no placeholder. Bumps apps/moderator/package.json 0.0.0 -> 0.0.1 to match the bootstrap tag, so the next `pnpm release:moderator` cuts 0.0.2 rather than colliding on an existing moderator-v0.0.1. Verified by building the image against BOTH this tree and the apps/moderator content on PR #3573, then running the container: /favicon.svg returns 200, / returns 302 to the auth hub with the correct returnUrl, and an unauthenticated POST /api/mod/* returns 401. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-03 15:52:34 -05:00
"release:moderator:patch": "node scripts/release-app.mjs apps/moderator moderator-v patch",
"release:moderator:minor": "node scripts/release-app.mjs apps/moderator moderator-v minor",
"release:moderator:major": "node scripts/release-app.mjs apps/moderator moderator-v major",
"yolo": "pnpm run release",
feat(scanner-policies): add moderator test bench for XGuard policy iteration Adds /moderator/scanner-policies, a moderator-only UI for authoring and scoring candidate XGuard policies against frozen test sets pulled from production moderator verdicts. Storage (no policies or results in the repo): - Candidates, system-prompt overrides, dataset records: sysRedis under REDIS_SYS_KEYS.SCANNER_POLICY (fail-open reads, fail-loud writes per system-cache.ts discipline) - Test workbooks + result merges: S3 (S3_UPLOAD_BUCKET, scanner-policies/ datasets/<mode>/<label>/...xlsx); one workbook per dataset, runs merge results into the same key in place Scoring (submit-and-callback, no synchronous wait): - startRun snapshots run state in sysRedis (candidates, rows, baseline, systemPrompt) and submits every (row × candidate) workflow with a callbackUrl pointing at /api/webhooks/scanner-policy-result. No wait, so the outer mutation returns in ms even for 2,500-call runs. - Webhook accumulates results in a sysRedis hash; counter increments atomically; finalizeRun fires when counter == total to build the xlsx (Results sheet merge by policyHash), upload back to S3, update the dataset's lastRun metadata, emit the terminal signal, and clean up. - Failed / cancelled / expired workflows still advance the counter so the run always finalizes; errors land in the Results sheet's errorMessage column. UI (Mantine v7 + tRPC): - Mode toggle (prompt / text), label sidebar, candidate editor with inline threshold + status edit - Per-mode system-prompt override panel (falls back to live xguard registry when unset) - Past datasets table with download / run / delete actions - Lazy SignalsProvider listener for progress (src/components/Signals/ ScannerPolicyTestSignal.ts) — attaches only when this page mounts Webhook + signals: - New SignalMessages.ScannerPolicyTestProgress - New /api/webhooks/scanner-policy-result with WEBHOOK_TOKEN guard + hExists idempotency check for re-delivered callbacks Dataset export: - Stratified sampling across TP/FP/TN/FN buckets from ScannerLabelReview joined with ScannerContentSnapshot (lower(label) match — DB stores lowercase while the registry is PascalCase) - Caps at user-specified max (default 500) with deterministic sort by contentHash so two exports of the same filter are byte-identical - Hidden _meta sheet preserves datasetId / mode / label across round trips Seed (one-time, idempotent): - scripts/seed-scanner-policies.ts populates the 12 prompt-mode + 15 text-mode live policies from xguard-manager export, plus the Young iterations explored this session (Options 4, 6, 11) Deps: + exceljs (server-side workbook io) Removed: docs/scanner-policies/, scripts/scanner-policy/ (CLI replaced by the in-app UI), .scanner-policy-cache/ gitignore entry
2026-06-01 18:22:23 -06:00
"seed:scanner-policies": "tsx --env-file=.env scripts/seed-scanner-policies.ts",
"prebuild": "pnpm build:workers",
2022-12-02 22:58:15 -07:00
"build": "next build",
"build:dev": "pnpm build:workers && cross-env NODE_OPTIONS=\"--max_old_space_size=16384\" next build",
Migrate to Next.js 15 Bump next 14.2.28 -> 15.5.19 (Pages Router app). Also bumps eslint-config-next and @next/eslint-plugin-next to 15.5.19, and eslint 8.22.0 -> 8.57.1 (Next 15's plugin rules use context.filename, added in ESLint 8.40). Config / API changes required by Next 15: - next.config.mjs: experimental.serverComponentsExternalPackages -> top-level serverExternalPackages; removed the now-stable experimental.instrumentationHook flag. - .eslintrc.js: added settings.next.rootDir so the Next ESLint plugin can locate the pages dir (otherwise the rules crash on load). - bot-detection.middleware.ts: dropped request.ip (NextRequest.ip removed in 15); cf-connecting-ip / x-forwarded-for fallbacks remain. - api/trpc/[trpc].ts: assert the default export as NextApiHandler. Next 15's route-type validator rejected it because withAxiom's NextConfig overload (declared first) structurally matches an arrow function. - Moved api/auth/oauth/__tests__/token.test.ts -> server/oauth/__tests__/token-endpoint.test.ts. Next 15 type-validates every file under pages/api as a route; the colocated test had no default export. Tests still pass (8/8). - Deleted the unused src/app/layout.tsx stub (whole app is Pages Router); removes the spurious "i18n unsupported in App Router" build warning. Build memory + Windows local-build helpers: - package.json build:dev / build:analyze heap 8192 -> 16384 (the app needs >8GB to compile). - scripts/graceful-fs-patch.cjs: optional NODE_OPTIONS --require preload to mitigate Windows EMFILE during local builds (no-op on Linux/CI). Validated: typecheck clean; lint at parity with main; build compiles, type-checks, passes route validation and page-data collection. The full production build's "Collecting build traces" step is FD-bound on Windows (EMFILE) and should be run on CI/Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:08:40 -06:00
"build:analyze": "cross-env NODE_OPTIONS=\"--max_old_space_size=16384\" ANALYZE=true next build",
ci(bundle): report-only First Load JS budget in the Dockerfile build (Tekton) (#2511) * ci(bundle): add report-only size-limit bundle budget job Next 16 removed per-route build stats, leaving no bundle-size regression signal. Adds a `bundle-budget` job to pr-check.yml that builds the app (SKIP_ENV_VALIDATION, no secrets) and runs size-limit over the shared client chunks (framework/main/webpack/_app + a coarse total) defined in .size-limit.json. Report-only for now: continue-on-error + intentionally loose limits. This is also the first GH Actions job to run a full `next build` (~8GB heap vs ~7GB standard runner) so early runs probe feasibility. Once a baseline is observed: tighten limits to baseline+headroom, drop continue-on-error, and make "Bundle Budget" a required check to gate. If Build OOMs, move to a larger runner label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Potential fix for pull request finding 'CodeQL / Workflow does not contain permissions' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> * ci(bundle): run size-limit in the Dockerfile build, not GH Actions Switch the bundle-size check from a separate GH Actions job (which would duplicate the full ~8GB next build) to a stage in the Dockerfile builder, right after `pnpm run build` where .next already exists. The Tekton buildkit build (preview + prod) now reports the size-limit numbers with no extra build — consistent with where app builds live. Report-only during the soak via `|| true` (numbers print to the build log). To gate later: drop `|| true` so a bundle regression fails the image build. Reverts the pr-check.yml bundle-budget job; keeps .size-limit.json + the size-limit deps + the `size` script. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(bundle): fix size-limit globs for Turbopack output The live preview build (next 16.2.7 Turbopack) revealed the webpack-era globs match nothing — Turbopack emits opaque hashed chunks (0--619vzepha0.js, turbopack-*.js), no framework-/main-/webpack-/_app- files. Those 4 entries errored ("can't find files"); only the recursive total worked. Baseline from the build: total client JS = 38.29 MB brotli (3615 chunks). Drop the 4 broken named-chunk entries; keep the working total with a 42 MB limit (~10% headroom). Still report-only (|| true in Dockerfile). Note: the coarse total is a weak regression signal under Turbopack's heavy code-splitting; a per-page First Load JS budget needs parsing .next/build-manifest.json (follow-up). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(bundle): manifest-based First Load JS budget (replaces size-limit) size-limit's globs can't see Turbopack's opaque hashed chunks, so it could only report a coarse 38 MB total (weak signal). Replace it with scripts/bundle-budget.mjs, which parses .next/build-manifest.json to reconstruct the metric Next used to print: First Load JS(route) = brotli(union(pages[route], pages["/_app"], polyfills)) shared-by-all-pages = brotli(pages["/_app"] + polyfills) Reports shared + total + the heaviest routes, checks .bundle-budget.json (report-only; `--gate` exits non-zero on a breach). No deps (Node stdlib zlib/fs), no extra build — still runs in the Dockerfile builder stage. Removes size-limit + @size-limit/file + .size-limit.json. Budgets are loose placeholders; tighten to baseline+headroom from the first build's printed First Load JS numbers, then add --gate + drop the `|| true` to enforce. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(bundle): tighten First Load JS budgets to baseline + headroom From build pr-preview-2511-fzrjd: shared-by-all = 425.9 kB, heaviest route (/user/[username]/models) = 1.13 MB. Set shared 470 kB (~10%) and routeMax 1.3 MB (~15%) so the report-only check is meaningful instead of passing trivially at the 1 MB/3 MB placeholders. Still report-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(bundle): bake bundle-budget report into the image for PR surfacing Write the size report to /app/bundle-budget.txt (still report-only, still printed to the build log) and COPY it into the runner image. A new Tekton bundle-comment task surfaces it on the PR via `kubectl exec ... cat` — no duplicate build. Uses redirect+cat instead of `| tee` so the script's exit code is preserved for the future --gate flip. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: retrigger preview build (bundle-comment task now live) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: retrigger preview (bundle-comment rollout-race fix live) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: retrigger preview (pr-deployer exec RBAC now granted) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(bundle): drop pnpm preamble from the bundle report Invoke `node scripts/bundle-budget.mjs` directly instead of `pnpm run size` so pnpm's lifecycle echo (`> model-share@… size /app`) stays out of /app/bundle-budget.txt and the PR comment. The `size` script stays in package.json for local use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: retrigger preview (collapsible bundle comment) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-06-13 15:26:47 -05:00
"size": "node scripts/bundle-budget.mjs",
"deploy": "pnpm run build && pnpm run db:deploy",
"postinstall": "pnpm run db:generate",
fix(typecheck): make a crashed typecheck report as crashed, not as clean (#3619) * fix(typecheck): make a crashed typecheck report as crashed, not as clean `pnpm run typecheck` was `cross-env NODE_OPTIONS="--max_old_space_size=8192" tsc --noEmit`. When the heap cap is too small for the program graph, V8 aborts part way through checking, so tsc emits ZERO diagnostics and dies. cross-env normalises the SIGABRT to exit 1, and V8's explanation goes to stderr — so a caller that captures stdout gets an empty log, a bare non-zero exit, and no type errors anywhere in it. That is indistinguishable from a clean pass to anything that judges the run by its output, which is what people and scripts actually do (a clean run also prints nothing). Reproduced with a deliberate `const x: number = 'nope'` in `src/`: at a 4096 MB cap the run reported 0 errors and hid it completely; the same tree at 8192 MB reported it. Measured cold on a clean checkout, with that error in place as a visibility control: node 24.18.1 4096 -> OOM/0 diags 4608 -> OOM/0 diags 5120 -> pass/found 8192 -> pass/found node 22.22.2 6144 -> pass/found 8192 -> pass/found So the current 8192 is NOT at the cliff — the cliff is between 4608 and 5120, and 8192 carries ~1.6x headroom. The number is left alone deliberately: the CI runner has 16 GB, and a cap near that trades a self-describing V8 abort for a kernel OOM-kill, which says less. Raising it would only move the cliff anyway. What changes is that crossing the cliff becomes loud. `scripts/typecheck.mjs` runs tsc and classifies the outcome: - clean -> prints an explicit "typecheck: OK" line, so silence is no longer what a pass looks like - type errors -> passed through untouched, exit code preserved - crashed -> a CRASHED banner naming the cause, on stdout AND stderr (the original blind spot was a stdout-only capture), plus a ::error:: annotation under Actions - exit 0 w/ diags -> treated as a crash rather than trusted Heap exhaustion, an outside kill (out of system RAM / a container limit) and an unexplained abort are named separately, because the fix differs — an outside kill wants a LOWER cap, not a higher one. The cap is passed as an argv flag rather than via NODE_OPTIONS so an inherited NODE_OPTIONS cannot override it. Override per-run with TYPECHECK_HEAP_MB=<mb>. Covered by scripts/__tests__/typecheck.test.ts, which drives the classifier with stub typecheckers (sub-second, vs minutes for a real run). Each of the five cases was mutation-checked against the wrapper: 6/6 mutations killed, each by its own test. One mutation initially SURVIVED and exposed a real gap in the test — the crash banner is written to stderr, so asserting only on stdout let a grep-poisoning regression through; both streams are asserted now. CI already invoked this via `pnpm run typecheck` and so inherits the wrapper; the step carries a comment against being "simplified" back to a bare tsc. * fix(husky): stop the pre-push hook echoing success over a failed typecheck The hook was: npm run typecheck echo "Typecheck successful" `sh` without `set -e` runs the next line regardless of what the previous one returned, and a script's exit status is its last command's — so the `echo` became the hook's verdict. A failing typecheck on `main` printed "Typecheck successful" and the push went through. Measured against the real hook in a throwaway repo on `main`, with a stub `npm` whose exit code is controlled: npm exit hook exit (before) hook exit (after) 0 0 0 1 0 1 134 0 134 Before, all three printed "Typecheck successful". The 134 row is the case this matters most for: that is V8 aborting on heap exhaustion, which emits no diagnostics at all, so the hook was echoing success over a typecheck that had not merely failed but never finished. The failure message points at scripts/typecheck.mjs, which distinguishes the two. The branch/username guard above is unchanged, and still makes the hook a no-op off `main`.
2026-08-04 13:48:02 -05:00
"typecheck": "node scripts/typecheck.mjs",
"lint": "eslint src/ --cache --cache-strategy metadata",
"lint:packages": "eslint packages --ext .ts",
2025-11-20 13:07:48 -07:00
"eslint": "cross-env TIMING=1 eslint src/ --quiet --cache --cache-strategy metadata",
"prettier:check": "prettier --check \"**/*.{ts,tsx}\"",
2022-10-13 10:26:04 -04:00
"prettier:write": "prettier --write \"**/*.{ts,tsx}\"",
"db:ui": "prisma studio",
2022-10-18 17:58:13 -06:00
"db:pull": "prisma db pull",
"db:push": "prisma db push",
"db:migrate": "node scripts/prisma-migrate-with-views-workaround.mjs",
"db:migrate:empty": "node scripts/create-empty-migration.mjs",
"db:applied": "node scripts/prisma-mark-migration-applied.mjs",
2023-10-12 12:30:05 -04:00
"db:deploy": "node scripts/prisma-migrate-with-views-workaround.mjs -p && npm run db:program",
2023-03-24 09:40:41 -06:00
"db:program": "node scripts/prisma-prepare-programmability.mjs",
"db:generate": "node scripts/generate-slim-schema.js && prisma generate --no-hints",
feat(db-queries): port moderation queries to Kysely + @updatedAt plugin Port the moderator/main-app moderation DB queries into the shared @civitai/db-queries package as executor-injected Kysely functions (entity-based modules, two-tier compile+EXPLAIN tests), and add a Kysely @updatedAt plugin so ported writes preserve Prisma's auto-bump. @updatedAt plugin: - updatedAtPlugin auto-stamps `updatedAt = <now>` on every UPDATE to a table with a Prisma `@updatedAt` column; the table set is generated from schema.prisma by a custom prisma generator (prisma-updated-at- generator.mjs -> updated-at-tables.ts), so it can't drift. - keepUpdatedAt opts a typed builder out of the bump (self-reference `updatedAt = "updatedAt"`); raw sql / INSERT / ON CONFLICT untouched. - Detects explicit updatedAt in both the object and chained .set(col,val) forms (ReferenceNode) — no double-stamp — and unwraps AliasNode so `updateTable('X as x')` is still recognized. - Wired into the app write/read clients (kyselyDb.ts); createKyselyClients gained a `plugins` option. compileHarness installs the plugin so tests mirror production. - Removed now-redundant explicit `updatedAt: new Date()` stamps from UPDATE writes on @updatedAt tables (plugin covers them); kept them for inserts, upsert ON CONFLICT, and non-@updatedAt tables. Deliberate no-bump writes (raw-SQL-sourced) use keepUpdatedAt. - db:check-generated script guards schema/generated-set drift. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 21:15:45 -06:00
"db:check-generated": "pnpm run db:generate && git diff --exit-code -- packages/civitai-db-schema/src",
2024-03-26 20:39:44 -06:00
"db:seed": "prisma db seed",
2024-07-25 09:40:42 -04:00
"share": "ngrok http 3000",
2024-11-20 14:44:42 -07:00
"prepare": "husky",
"analyze": "cross-env ANALYZE=true next build",
"analyze:server": "cross-env BUNDLE_ANALYZE=server next build",
"analyze:browser": "cross-env BUNDLE_ANALYZE=browser next build",
2025-07-09 13:38:50 -06:00
"tsc:trace": "cross-env NODE_OPTIONS=\"--max_old_space_size=8192\" tsc --generateTrace ./trace --incremental false",
"tsc:analyze": "npx analyze-trace trace",
"test": "cross-env NODE_ENV=development npx playwright test",
"test:ui": "cross-env NODE_ENV=development npx playwright test --ui",
2025-02-10 15:17:01 -05:00
"test:gen": "cross-env NODE_ENV=development npx playwright codegen",
2025-03-06 16:41:39 -04:00
"test:reset": "make bootstrap-db",
test(component): Vitest browser-mode component-testing scaffold (runs in Tekton, removes GH Actions pr-check) (#2547) * test(component): add Vitest browser-mode component-testing scaffold + SeedInput Adds a second Vitest project (`component`, browser mode / real Chromium via Playwright) alongside the unchanged 857-test `unit` suite. Includes a renderWithProviders scaffold (Mantine + QueryClient + next/router mock), a process.env shim for browser mode, a report-only GH Actions `component-tests` job, and the first test on the high-churn, e2e-impossible SeedInput generation leaf (6 cases, mutation-proven). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(component): address audit findings (typecheck test/, seed-test teeth, optimizeDeps) - tsconfig: add `test` to include so the load-bearing browser-process-shim + component-setup are actually typechecked (were only checked transitively/not at all). (audit H1) - SeedInput test: stub Math.random for an exact-value assertion bounded by MAX_RANDOM_SEED (was a loose MAX_SEED range that survived a constant-seed mutation). (audit M1) - vitest component project: optimizeDeps.include next/router to stop the "Vite unexpectedly reloaded a test" flake warning. (audit L4) - drop the stale "857 tests" count from the config comment. (audit M2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: remove GitHub Actions pr-check.yml — consolidate PR checks onto Tekton typecheck + unit-tests already run in the Tekton PR-preview pipeline (author- gated on MEMBER/OWNER/COLLABORATOR), and component-tests now run there too (talos-infra: report-only npm-component-tests task). Removing this workflow stops paying for GitHub Actions runner time. Tradeoff: external-contributor PRs (not author-authorized) no longer get automated checks — accepted per the "pure Tekton" decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:55:14 -05:00
"test:unit": "vitest --project unit",
"test:unit:run": "vitest run --project unit",
"test:unit:coverage": "vitest run --project unit --coverage",
ci: run the nine packages/* test suites, which nothing has ever executed (#3642) * ci: run the nine packages/* test suites, which nothing has ever executed The root `unit` Vitest project's `include` is root-relative (`src/**`, `scripts/**`), and the `Unit tests` job runs `vitest run --project unit`. So no CI job in this repo has ever invoked a workspace package's suite: 616 tests across nine `packages/*` packages ran only for whoever remembered `pnpm --filter <pkg> test` by hand. That is not theoretical. The schema-drift detector (#3591) shipped 81 tests into this gap, and two of them had been RED on `main` since #3592 landed — with nothing anywhere to say so. Root `vitest.config.mts` now globs each package's OWN config file, so every package keeps the config it was written against. The new `Package unit tests` job is BLOCKING: the reasoning that made `Unit tests` report-only does not transfer, because this is 616 tests with no browser, no database and no Next module graph, and the whole run is ~15s. Three pre-existing failures the job surfaced, all fixed here rather than skipped: * civitai-db-queries/src/infra/enum-array-parsers.explain.test.ts could never pass without a database. `describe.skipIf(!url)` skips the tests but Vitest still EXECUTES the describe callback during collection, so the eager `new Pool({ connectionString: noVerify(undefined) })` threw `TypeError: Invalid URL` and failed the whole FILE to import — which reports as "1 failed test file" with no test count, not as a failing test. The Pool moves into `beforeAll`, which genuinely does not run when skipped. * production-snapshot.test.ts pinned 235 nullability findings on seven `*Rank` tables. #3592 marked those columns optional to match the database and the findings correctly went away. The assertion now guards the REMEDIATION (Rank family at zero) with a positive control on that zero — the 11 findings that remain. * parse-prisma-schema.test.ts pinned Prisma's defaulted `onDelete` at one named site, `TagsOnImageNew.image`, and #3589 gave that relation an explicit `onDelete`. Re-anchored to the POPULATION of bare relations, which an ordinary schema edit cannot retire. A green vitest run is a claim, not evidence: `--project` matching nothing exits 0, and so does a config whose globs stopped resolving. scripts/ci/assert- package-suites-ran.mjs therefore asserts a ledger — every package with a vitest config and a test file on disk must appear in the results — so the job fails when the executed set SHRINKS, which the totals cannot see. Validated against three known-bad reports (empty run, one package dropped, missing report file) before being trusted. Test count, measured in CI: 811 files / 12,031 tests before, all from `src/`; 61 files / 616 tests added by the new job, of which 81 are the drift detector's. * fix(ci): correct the "blocking" claim, and close three ledger blind spots Audit follow-ups on the packages/* CI job. The job was described as BLOCKING. It is not, and the distinction is load-bearing for whoever next decides whether to flip `unit`: `main` has branch protection but no required_status_checks at all, so a red check here does not prevent a merge. What it actually buys is rendering RED instead of red-but-ignored. Corrected in the workflow comment, the README and the PR body, along with what would make it a real interlock. The Rank positive control was green for the wrong reason. Asserting the Rank family is at zero nullability findings is satisfied just as well by a differ that never visits a Rank table — measured: making compare.ts `continue` on every model whose table ends in `Rank` left that test GREEN and reddened only two neighbouring ones. The `nullability.length >= 11` control could not see it, because it proves the KIND is still emitted, not that those seven tables are in scope. Now pinned via findings of a different kind that the same seven models must still produce; re-running the identical mutation kills the test itself. Three ledger blind spots, each demonstrated with a probe before and after: - It only looked in `src/`. A package with its tests in a top-level `__tests__/` was invisible to the EXPECTED set, so it could never be noticed going missing. Now scans the whole package directory. - It only matched `.test.ts`/`.spec.tsx`. `.mts`, `.cts` and `.js` tests were invisible the same way. - It counted SKIPPED tests as having run, summing assertionResults.length. A package that self-skipped in its entirety still satisfied the ledger — and self-skipping on a missing DATABASE_URL is exactly the pattern these suites use, so that failure mode is live, not hypothetical. It now requires a non-zero EXECUTED count and reports skips beside it, so a package quietly turning itself off is visible rather than absent from the arithmetic. Both directions verified by exit code, not by reading output: real report 0; tests-outside-src probe 1; .mts probe 1; wholly-skipped package 1; empty run 1; one package dropped 1; missing report 1. Known remaining gap, deliberately not widened into this change: `apps/*` has four more vitest configs and ~43 test files that no CI job runs. Same one-line fix in the same projects array, plus teaching the ledger about `apps/` — it hardcodes `packages/` today. Follow-up PR. Also: the CI job's JSON report is gitignored (the documented command left an untracked artifact at the repo root), and the workflow comment's "four EXPLAIN tests" is corrected to 3 + 1 across two files.
2026-08-05 19:55:32 -05:00
"test:packages": "vitest --project '@civitai/*'",
"test:packages:run": "vitest run --project '@civitai/*'",
ci(test): run the apps/* vitest suites — 369 tests CI had never executed (#3694) The root vitest.config.mts registered `packages/*/vitest.config.*` as projects and nothing for `apps/*`, and CI runs only `--project unit` (root-relative `include`) and `--project '@civitai/*'`. So no test under apps/ has ever run in CI: 43 files / 369 tests across five apps, green only for whoever remembered `pnpm --filter <app> test` by hand. This is the same gap #3591 documented for packages/, in the sibling directory, missed when that one was closed. Register `apps/*/vitest.config.{ts,mts}` — globbed on the CONFIG FILE for the reason the packages comment already gives — and give each app project an explicit `app:` name. That name is load-bearing: every app is also published as `@civitai/*` (`@civitai/auth-app`, and `@civitai/orchestrator-gateway` with no suffix), so under default naming no pattern separates apps from packages and `--project '@civitai/*'` would have silently swept the apps into the packages job. Verified the packages selector is unchanged: same 9 packages, same 957 tests, zero app files in the report. apps/creator-studio gets a config it never had — it declared a `test` script and owned a vitest test file that no runner could reach. Generalize scripts/ci/assert-package-suites-ran.mjs to take the workspace dir and rename it to assert-workspace-suites-ran.mjs, so one ledger serves both jobs, and wire an `App unit tests` job that runs it. Per-app counts: auth 29 files/237, notifications 10/101, storage 1/17, orchestrator-gateway 2/6, creator-studio 1/8. All green. Positive controls (both with vitest exiting 0, assertion exiting 1 on its own `missing` branch): breaking one app's `include` so it collects nothing, and dropping one app's `test.name` so the selector no longer matches it. Also corrects an inherited premise in the script header — on vitest 4.0.18 a `--project` filter matching nothing fails startup and exits 1, it does not exit 0 as the comment claimed. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 12:52:36 -05:00
"test:apps": "vitest --project 'app:*'",
"test:apps:run": "vitest run --project 'app:*'",
test(browser): single-source the loadable image fixture + lint the unloadable-URL trap (#3553) * test(browser): single-source the loadable image fixture + lint the unloadable-URL trap #3551 fixed an AppBlockChrome test that had been red on `main` across five PRs: its icon fixture was an http URL, which cannot load in the test browser, so the `<img>` fired a real `error` event and Mantine 7.17.8's Avatar (`useState(!src)` + `onError -> setError(true)`) destroyed it ~11 ms after mount. The `expect(img).not.toBeNull()` assertion was racing that window. The fix was a `data:` URI — and it was the SECOND copy of that constant, with the same explanatory comment, in the repo. AppListingCard already had one. The convention existed and was invisible, which is why it took five PRs to notice. Two changes: 1. One fixture. `LOADABLE_IMAGE_DATA_URI` now lives in `test/component-setup.tsx` — the shared browser-test scaffold that 115 of the 117 `*.browser.test.tsx` files already import for `renderWithProviders`, so no new convention and no new import site. AppBlockChrome's `LOADABLE_ICON` and AppListingCard's `LOADABLE_PNG` both import it; the explanation lives with the constant. 2. A deterministic guard. New `local-rules/no-unloadable-image-fixture`, scoped to `**/*.browser.test.tsx`, flags an http(s) URL literal in an image-source position. Escape hatch is the same one the repo's other local rules use: `// eslint-disable-next-line local-rules/no-unloadable-image-fixture -- <reason>`. Population, measured rather than assumed — all 117 browser tests were run with a document-level capture listener recording every `<img>` that fires a real `error` event with an http(s) src: 117 *.browser.test.tsx files 39 contain any http(s) string literal 14 distinct external image URLs really do mount as broken <img>s today, across 6 files (positive control: the listener recorded exactly the 3 URLs AppListingCard is known to mount) 1 file has BOTH an http image fixture AND an <img>-existence assertion (AppListingCard — fixed here) So the other fixtures are latent, not harmless: each becomes a flake the day someone adds an `<img>` assertion beside it, which is exactly what happened to AppBlockChrome. Proportionality. The rule reports 16 sites in 5 files, all pre-existing. That is survivable for the same reason spelled out on `no-wholesale-module-mock`: lint.yml gates ADDED files blocking and MODIFIED files report-only, so the backlog can only ever reach the report-only step while a NEWLY ADDED browser test is blocked — the authoring path the rule exists to close. Severity is 'error' for the same reason too: the blocking step runs without --max-warnings, so 'warn' would gate nothing. False positives. Two independent conditions must both hold (http(s) literal AND an image-source position), and the ambiguous `src` key additionally has to prove it is an image via a file extension or an `<img>`-family JSX element. Verified clean on every real non-image http URL in the browser suite: OnsiteReviewModal's `iframe: { src: 'https://example.com/block' }`, AgentReviewChat's markdown `![tracking](https://example.com/pixel.png)` (whose test asserts NO `<img>` is produced), `liveUrl` / `externalUrl` / `previewUrl` / `reviewRepoUrl`, and the relative `/api/blocks/screenshot/...` paths in AppBlockCard / AppDetailsModal. Mutation evidence (rule): - 34 RuleTester cases (18 valid / 16 invalid), added to `pnpm test:lint-rules` - rule made a no-op -> invalid cases fail ("Should have 1 error but had 0") - `src` image-proof removed -> 4 valid cases fail (iframe/script src false-positive) - http matcher widened -> 2 valid cases fail (data: URI / relative path) - key membership removed -> extensionless `iconUrl` case stops firing - real eslint on a synthetic browser test: 7 reports across every covered shape, 0 on every legitimate shape - AppListingCard's disable comment removed -> 1 report; restored -> 0 (the hatch is load-bearing, not decorative) Escape hatch used in exactly one place: AppListingCard's deliberate broken-cover test, which needs a URL that really fails so it can exercise the component's onError -> placeholder path. Its assertions all run on the post-error state behind a `vi.waitFor`, so it never races the swap. Behaviour is unchanged — only comments and the disable directive were added around an identical fixture URL. Verification: AppBlockChrome + AppListingCard 63 passed, identical to the pristine `origin/main` baseline (63 passed); 112 passed across those two plus two untouched browser tests, confirming the shared scaffold change is inert; 186 passed across all four local-rule suites. Not claimed: swapping AppBlockChrome's fixture back to an http URL did NOT turn the test red on this machine. That is consistent with the defect being a race that manifests under CI load rather than locally, and it is why a convention was never going to hold — but it means the original symptom was not reproduced here. * docs(lint): correct the rule's mechanism claim and state its coverage honestly Audit corrections. Comments only — the rule's behaviour is unchanged and its 34 RuleTester cases still pass. 1. The header claimed "every image-rendering component in this codebase has an onError fallback that then DESTROYS the <img>". False, and it was the rationale a maintainer would read when deciding to widen or delete the rule. Checked against the installed @mantine/core: - Avatar (Avatar.mjs:70,91) renders a <span> placeholder INSTEAD of the <img> — destroyed, ~11 ms after mount. Same for a bespoke onError -> placeholder (AppListingCard.tsx:135). - Image (Image.mjs:58) only swaps when `fallbackSrc` is set, and that prop appears ZERO times in src/ — otherwise it re-renders the SAME <img>, so the element survives with a failed src. Only the first group can flake an <img>-existence assertion. 2. Coverage is partial and the PR implied otherwise by juxtaposing "14 mounting URLs" with "16 reported sites". The rule reports 9 distinct URLs of those 14, so >=5 (~36%) are outside it. Documented the shape of the gap (`url` is the most common http-literal binding at 36 occurrences and is deliberately excluded) and that widening behind the existing IMAGE_EXT_RE proof is a deferred one-liner, not an oversight. 3. "Blast radius zero" is true today but not permanently: --diff-filter=A treats a rename-with-heavy-edit as ADDED (so moving a backlog file lands it in the blocking step), and lint.yml records that the report-only steps are planned to flip to blocking. Both named in the override comment. 4. AppListingCard's "every assertion below is post-error" was wrong — the cover-ratio-box assertion runs before the vi.waitFor. It is not a race (that box survives the swap), but the comment claimed more than the code did. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-02 23:15:10 -05:00
"test:lint-rules": "vitest run --project unit src/server/services/__tests__/no-io-in-transaction.test.ts src/server/services/__tests__/no-module-scope-cache.test.ts src/server/services/__tests__/no-unloadable-image-fixture.test.ts src/server/services/__tests__/no-wholesale-module-mock.test.ts",
test(component): Vitest browser-mode component-testing scaffold (runs in Tekton, removes GH Actions pr-check) (#2547) * test(component): add Vitest browser-mode component-testing scaffold + SeedInput Adds a second Vitest project (`component`, browser mode / real Chromium via Playwright) alongside the unchanged 857-test `unit` suite. Includes a renderWithProviders scaffold (Mantine + QueryClient + next/router mock), a process.env shim for browser mode, a report-only GH Actions `component-tests` job, and the first test on the high-churn, e2e-impossible SeedInput generation leaf (6 cases, mutation-proven). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(component): address audit findings (typecheck test/, seed-test teeth, optimizeDeps) - tsconfig: add `test` to include so the load-bearing browser-process-shim + component-setup are actually typechecked (were only checked transitively/not at all). (audit H1) - SeedInput test: stub Math.random for an exact-value assertion bounded by MAX_RANDOM_SEED (was a loose MAX_SEED range that survived a constant-seed mutation). (audit M1) - vitest component project: optimizeDeps.include next/router to stop the "Vite unexpectedly reloaded a test" flake warning. (audit L4) - drop the stale "857 tests" count from the config comment. (audit M2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: remove GitHub Actions pr-check.yml — consolidate PR checks onto Tekton typecheck + unit-tests already run in the Tekton PR-preview pipeline (author- gated on MEMBER/OWNER/COLLABORATOR), and component-tests now run there too (talos-infra: report-only npm-component-tests task). Removing this workflow stops paying for GitHub Actions runner time. Tradeoff: external-contributor PRs (not author-authorized) no longer get automated checks — accepted per the "pure Tekton" decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:55:14 -05:00
"test:component": "vitest run --project component",
"test:component:watch": "vitest --project component",
2025-05-29 16:27:30 -06:00
"meilisearch:migrate": "NODE_ENV=development tsx scripts/oneoffs/meilisearch-migration.ts",
"tsscript": "NODE_ENV=development tsx",
2025-05-29 16:27:30 -06:00
"madge:orphans": "madge --orphans --image ./public/orphans-graph.svg --ts-config ./tsconfig.json --extensions ts,tsx src/",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"depcheck": "depcheck",
2025-11-24 12:11:58 -04:00
"generate-types": "typed-scss-modules src",
"ts-script": "NODE_ENV=development tsx"
2022-10-11 16:56:51 -04:00
},
2022-10-14 11:05:04 -06:00
"prisma": {
"schema": "packages/civitai-db-schema/prisma/schema.prisma",
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} packages/civitai-db-schema/prisma/seed.ts"
2022-10-14 11:05:04 -06:00
},
2022-10-11 16:56:51 -04:00
"dependencies": {
"@aws-sdk/client-s3": "^3.490.0",
"@aws-sdk/lib-storage": "^3.490.0",
"@aws-sdk/s3-request-presigner": "^3.490.0",
2023-07-03 11:13:10 -06:00
"@axiomhq/axiom-node": "^0.12.0",
feat(app-blocks): Phase 3 host wiring for per-account buzz (#2893) * feat(app-blocks): Phase 3 host wiring for per-account buzz Wires the civitai host (Phases 1 & 2 already merged/published) to the per-account-buzz feature: - Bump @civitai/app-sdk ^0.6.0→^0.14.0 and @civitai/blocks-react ^0.4.0→^0.16.0 (targeted lockfile update, no unrelated churn). - GET_BUZZ_BALANCE handler on PageBlockHost + IframeHost → the block-token-authed blocks.getMyBuzzBalance MUTATION, replying BUZZ_BALANCE_RESULT with { blue, green, yellow } on success and the error variant on failure / null-token (never hangs the block). - accountType rides through SUBMIT_WORKFLOW (body passed wholesale to submitWorkflow; server-side domain-clamps), and the realized spentAccountType on the returned snapshot reaches the block unaltered. - Parity inventory: add GET_BUZZ_BALANCE (required on both real hosts). The 0.6→0.14 bump also newly PUBLISHED OPEN_RESOURCE_PICKER (previously ahead-of-published), which the one-directional compile-time gate now requires — added to INVENTORY (PageBlockHost required; IframeHost N/A, model slot uses the narrower OPEN_CHECKPOINT_PICKER). - Tests: extend PageBlockHostWorkflow.browser.test.tsx (balance success/ error/null-token/no-requestId + accountType forwarding + spentAccountType surfacing); add getMyBuzzBalance to the sibling hosts' trpc mocks. Phase 4 (scaffold UI) is the last phase; this completes the end-to-end path once merged + deployed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(blocks): cover IframeHost GET_BUZZ_BALANCE handler + note guard asymmetry (audit #2893) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:35:31 -05:00
"@civitai/app-sdk": "^0.14.0",
"@civitai/auth": "workspace:*",
"@civitai/buzz": "workspace:*",
"@civitai/client": "0.2.0-beta.86",
"@civitai/cybertipline-tools": "^0.1.0",
"@civitai/db-queries": "workspace:*",
"@civitai/db-schema": "workspace:*",
"@civitai/moderation": "workspace:*",
"@civitai/next-axiom": "^0.17.0",
"@civitai/shared": "workspace:*",
2025-06-13 09:01:27 -04:00
"@clavata/sdk": "^0.2.3",
2023-09-21 15:20:15 -06:00
"@clickhouse/client": "^0.2.2",
"@coinbase/cdp-sdk": "^1.13.0",
"@discordjs/rest": "^2.6.0",
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
2022-10-11 16:56:51 -04:00
"@emotion/react": "^11.10.4",
2023-04-10 18:15:45 -06:00
"@essentials/one-key-map": "^1.2.0",
2025-09-04 13:04:08 -05:00
"@flipt-io/flipt-client-js": "^0.2.0",
2025-07-15 11:45:04 -06:00
"@floating-ui/dom": "^1.6.0",
"@google-cloud/recaptcha-enterprise": "^5.1.1",
feat(faro): frontend RUM Phase 1 (dark) — errors + web-vitals + sampled tracing (#2929) * feat(faro): frontend RUM Phase 1 (dark) — errors + web-vitals + sampled tracing Wire the Grafana Faro Web SDK into the Pages Router app, shipped DARK: Faro only initialises when NEXT_PUBLIC_FARO_ENABLED is true AND NEXT_PUBLIC_FARO_COLLECTOR_URL is set AND the runtime `faro` feature flag is on. Session replay is OFF; console capture is OFF. - deps: @grafana/faro-web-sdk + @grafana/faro-web-tracing (2.8.2), pinned @opentelemetry/sdk-trace-web to Faro's resolved 2.9.0 (single otel singleton). - env: NEXT_PUBLIC_FARO_{ENABLED,COLLECTOR_URL,TRACES_SAMPLE_RATE,SESSION_SAMPLE_RATE}, all optional/defaulted (never required-in-prod → fresh PR-preview builds stay green). - redact.ts: deterministic, unit-tested PII scrub (redactUrl/redactText/deepRedact) — strips token/code/key/signature/email/secret/session/otp/verify/password params + emails/JWTs/long tokens from every beacon via beforeSend. The primary privacy control (replay off is not enough). 17 Vitest cases. - FaroProvider: errors + web-vitals (100%); browser tracing via TracingInstrumentation; traceparent propagated to SAME-ORIGIN /api only; ignoreErrors storm guard; idempotent init guard (StrictMode/HMR). - faroTracing.ts: genuine per-trace TraceIdRatioBasedSampler (default 0.1) via a custom spanProcessor — Faro couples trace sampling to session sampling (which gates ALL signals), so session stays 1.0 for 100% errors/web-vitals and traces are sub-sampled per-trace here. - feature flag: `faro` default OFF (mods only), Flipt-toggleable. - No CSP change (Phase 1, per plan B5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(faro): Phase 1 — traces follow session sampling; drop custom spanProcessor Per decision: simplify Phase 1. Remove faroTracing.ts (the per-trace TraceIdRatioBasedSampler that reconstructed Faro's internal export pipeline) and use the default TracingInstrumentation, so browser traces follow session sampling. Drop the now-unused @opentelemetry/sdk-trace-web direct dep (Faro brings its own transitively). NEXT_PUBLIC_FARO_TRACES_SAMPLE_RATE stays in the schema (build-env plumbing for later) but is marked RESERVED/inert in FaroProvider — genuine per-trace sampling must be wired before widening past the mod cohort (Faro couples per-trace sampling to session sampling; a non-sampled session drops ALL signals, so session sampling can't sub-sample traces without also dropping errors/web-vitals). Fine while dark/mod-only (trace volume negligible). typecheck clean for changed files; 17/17 redact tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(faro): privacy-audit hardening — close reproduced redaction leak paths Pre-merge PII audit of the dark RUM pipeline found real leak vectors. All fixed before any flag flip: - F1: OTLP trace-span URL attributes escaped redaction. redact.ts MAX_DEPTH 8→24 so `resourceSpans[].scopeSpans[].spans[].attributes[].value.stringValue` (~depth 10) is reached, and `stringValue` added to the url-aware key set → traced fetch/xhr URLs with ?code=/?signature= are scrubbed (span + span-event attrs). New test proves it. - F2: page.url got the weaker scrub (redactUrl only). scrubBeacon now uses redactText(redactUrl(page.url)), mirroring deepRedact's url-key treatment → an email/JWT in a URL PATH SEGMENT is redacted on every beacon. New tests prove it. - F3: beforeSend failed OPEN (returned the unredacted item on scrub error). Now FAILS CLOSED — scrubBeacon catch returns null (Faro drops null items: verified `map(hook).filter(Boolean)` in faro-core 2.8.2), plus a defensive outer wrapper. - F4: stopped using getWebInstrumentations() (always bundles UserAction + Performance + CSP + Console). Explicit allow-list: errors, web-vitals, session, view, navigation, tracing. Excludes Performance (resource URLs), UserAction (element datasets), CSP, Console. Comment now states the true set. - F5: corrected the kill-switch claim — flag-off takes effect on next page load; added best-effort faro.pause() on an enabled true→false transition for open tabs; infra ingress is the immediate cluster-wide stop. - F6: comment forbidding faro.api.setUser()/meta.user without extending the scrub. - F8: +3 redact tests (OTLP http.url span/event attr; email- and JWT-in-path page.url). typecheck clean for changed files; redact tests 20/20 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(faro): close 3 residual audit findings (encoded-email, whole-meta scrub, comment) Re-audit residuals — all cheap, keep the gate robust for widening: 1. traceparent comment corrected (comment-only, no logic change): sdk-trace-web attaches traceparent to ALL same-origin requests; propagateTraceHeaderCorsUrls only gates CROSS-origin. Security property is unchanged (a same-origin matcher can't match a cross-origin URL → third parties never get the header) — the comment now says so. 2. %40-encoded email in a benign-named query param (e.g. ?u=a%40b.com) survived the literal-@ EMAIL_RE. Added EMAIL_ENCODED_RE to redactText. +1 regression test. 3. scrubBeacon now runs deepRedact over the WHOLE meta (not just meta.page), so PII in meta.session/view/browser/app attributes is caught too; stable ids/UA/version pass through (pattern-based redaction only). setUser guard comment extended to name session/view attributes. +1 regression test (planted meta.session note email). typecheck clean for changed files; redact tests 22/22 green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:10:17 -05:00
"@grafana/faro-web-sdk": "2.8.2",
"@grafana/faro-web-tracing": "2.8.2",
2025-02-05 16:44:31 -07:00
"@headlessui/react": "2.2",
2025-07-02 10:28:33 -06:00
"@hookform/resolvers": "^5.1.1",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"@mantine/core": "^7.17.7",
"@mantine/dates": "^7.17.7",
"@mantine/dropzone": "^7.17.7",
"@mantine/hooks": "^7.17.7",
"@mantine/modals": "^7.17.7",
"@mantine/notifications": "^7.17.7",
"@mantine/nprogress": "^7.17.7",
"@mantine/tiptap": "^7.17.7",
"@marsidev/react-turnstile": "^1.0.1",
2024-04-24 15:29:23 -06:00
"@meilisearch/instant-meilisearch": "0.13.5",
2023-08-17 15:00:34 -04:00
"@microsoft/signalr": "^7.0.10",
chore(deps): upgrade to Next 16.3.0 and stop paying nested-async chunking in dev (#3742) * perf(dev): stop paying server nested-async chunking in dev The flag exists to shrink emitted server chunks in a production build. A dev server never emits those chunks, so the walk over dynamic-import paths is pure cost there, on a module graph `_app` already makes large. Keeps the build-time behaviour from #3458 unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(deps): upgrade next to 16.3.0 16.3.0 is the first release carrying vercel/next.js#93788, which fixes a lock-order inversion in turbo-tasks-backend and removes an incorrect `unsafe impl Send` on dash_map_multi::RefMut that let a caller hold a shard write guard across an await. The symptom is a dev server that pegs most of the cores with flat memory and never finishes compiling a route, recoverable only by restart. Verified by reading the file at each tag: the unsound impl is present through 16.2.12, the last 16.2 release, and gone in 16.3.0. It was never backported, so no 16.2.x can pick it up. The `get_multiple_mut` equal-keys assert still exists in 16.3.0, so this is not expected to silence that panic — only to stop a panicking worker from wedging every other worker behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: commit the agent-rules block next dev writes into CLAUDE.md `next dev` on 16.3.0 appends a `nextjs-agent-rules` block to CLAUDE.md and re-adds it every run (next/dist/server/lib/generate-agent-files.js), so the choice is to commit it or carry an uncommitted change forever. It earns its place here: it points at `node_modules/next/dist/docs/`, which is already the authority this repo cites for Turbopack defaults, and the warning is aimed squarely at a pages-router app on Next 16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:55:51 -06:00
"@next/bundle-analyzer": "^16.3.0",
2024-11-16 15:26:12 -07:00
"@next/third-parties": "^15.0.3",
feat(oauth): scoped tokens, OAuth 2.0 server, per-subject buzz limits Squashed merge of feature/scoped-tokens onto latest main. OAuth 2.0 server (authorization code + PKCE, refresh, revoke, device flow, OIDC discovery), bitwise TokenScope enum (25 flags) with a fail-safe enforceTokenScope middleware (un-annotated procedures default to requiring Full), 83 routers annotated, 15 buzz-spending procedures gated with blockApiKeys: true. Per-subject buzz limits: opaque (type, id) subject pair, BuzzBudget[] shape supporting absolute/sliding/rollover variants with optional currency filters, stored on ApiKey.buzzLimit (User-type keys) or OauthConsent.buzzLimit (OAuth grants — stable across access-token rotations). Civitai stores limits + busts cache + cleans up subjects via /v1/manager/users/:userId/{auth, limits/auth}/:type/:id; orchestrator owns enforcement and rolling-window math. Account UI: card-based ApiKeys, OAuthApps, ConnectedApps surfaces with inline spend bars + a shared EditBuzzLimitModal. OAuth consent screen collects an optional buzz limit when AIServicesWrite is requested. OAuth Apps + Connected Apps gated behind the `oauth-apps` Flipt flag (mod-only). Audit via the existing ClickHouse `actions` table (BuzzLimit_Set ActionType). DB: one new migration 20260507165710_add_buzz_limit_to_oauth_consent adds OauthConsent.buzzLimit JSONB. Legacy KeyScope[] column drop is deferred to a follow-up PR after this is stable in prod. Demo client: civitai/civitai-oauth-demo (separate repo). Conflict resolution during the rebase onto main: kept main's newer multi-image candidate handling in comics.router.ts (it was a substantive content divergence, not a metadata conflict; the blockApiKeys: true annotation on purchaseChapterAccess was already preserved through the non-conflicting merge regions). Prisma types regenerated post-merge. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:07:41 -06:00
"@node-oauth/oauth2-server": "^5.3.0",
2025-03-08 16:41:38 -05:00
"@number-flow/react": "^0.5.7",
2025-06-09 13:05:39 -04:00
"@okikio/sharedworker": "^1.1.0",
Challenge Platform - Phase 1 Complete (#1957) * Add challenge platform proposal document Comprehensive proposal for overhauling the daily challenge system: - New Challenge, ChallengeEntry, ChallengeWinner tables - Replace Article-based challenges with dedicated entity - Support for multi-day and concurrent challenges - User-created challenges with custom judging prompts - OpenRouter SDK migration for LLM flexibility - Prompt Lab for testing judging criteria - Prize + operation cost escrow system Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add OpenRouter SDK integration for unified LLM access - Install @openrouter/sdk package - Create openrouter.ts abstraction layer with: - AI_MODELS constants for common models - SimpleMessage type for easy message construction - getJsonCompletion helper with retry logic - Automatic conversion to SDK message format - Add OPENROUTER_API_KEY to server env schema - Migrate generative-content.ts from OpenAI to OpenRouter - Update imports and client references - Use AI_MODELS.GPT_4O for model selection - Add proper type narrowing with 'as const' This enables model flexibility and fallback routing through OpenRouter's unified API while maintaining the same interface. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add Challenge system database models New Prisma models: - Challenge: Core challenge entity with timing, content, prizes, and lifecycle - ChallengeEntry: User submissions with AI scoring - ChallengeWinner: Winner records with placement and rewards New enums: - ChallengeSource: System, Mod, User - ChallengeStatus: Draft, Scheduled, Active, Judging, Completed, Cancelled - ChallengeEntryStatus: Pending, Accepted, Rejected, Scored Added relations to existing models: - User: challengesCreated, challengeEntries, challengeEntriesReviewed, challengeWins - Image: challengesCover, challengeEntries, challengeWins - Model: challenges - ModelVersion: challenges - Collection: challenges This replaces the Article-based challenge system with dedicated entities supporting multi-day challenges, variable duration, and user-created challenges. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add database migration for Challenge system Creates the following database objects: - ChallengeSource enum (System, Mod, User) - ChallengeStatus enum (Draft, Scheduled, Active, Judging, Completed, Cancelled) - ChallengeEntryStatus enum (Pending, Accepted, Rejected, Scored) - Challenge table with all fields and indexes - ChallengeEntry table with unique constraint on (challengeId, imageId) - ChallengeWinner table with unique constraint on (challengeId, place) - Foreign key relationships to User, Image, Model, ModelVersion, Collection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Implement Challenge system dual-write and migration - Add challenge-helpers.ts with new Challenge table CRUD operations: - getChallengeById, getActiveChallengeFromDb, getScheduledChallengeFromDb - createChallengeRecord, updateChallengeStatus, setChallengeActive - createChallengeEntry, updateEntryStatus, getChallengeEntries - createChallengeWinner, getChallengeWinners, closeChallengeCollection - Update daily-challenge-processing.ts with dual-write: - Create Challenge records alongside Article records - Update Challenge status to Active when challenge starts - Create ChallengeWinner records when picking winners - Update Challenge status to Completed when challenge ends - Update daily-challenge.service.ts with new functions: - getAllChallengesFromDb for querying Challenge table - getVisibleChallenges for public challenges feed - Re-export getChallengeById - Add migrate-challenges.ts job to migrate existing Article-based challenges to the new Challenge table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add challenge platform UI components and API Phase 1 implementation includes: - ChallengeCard component for feed display - ChallengesInfinite component with masonry grid - /challenges feed page with status/sort filters - /challenges/[id] detail page with winners, entries, and sidebar - /moderator/challenges management page with CRUD operations - challenge.router.ts with public queries and moderator mutations - challenge.schema.ts with all input validation schemas - challenge-auto-queue job for 30-day horizon monitoring Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add moderator challenge create/edit pages and theme preview API - ChallengeUpsertForm: Full form for creating/editing challenges - Basic info: title, theme, invitation, description - Model selection using Meilisearch for efficient search - Schedule configuration: visibleAt, startsAt, endsAt - Prize configuration with 3-place prizes + participation prize - Review settings: maxEntries, reviewPercentage, operationBudget - Status and source controls - ModelSearchInput: Autocomplete component using Meilisearch API - Debounced search (300ms) - Shows model name + creator - Efficient - no database queries, uses search index - /moderator/challenges/create: New challenge creation page - /moderator/challenges/[id]/edit: Challenge editing page - getUpcomingThemes API: Returns upcoming visible challenges - For theme preview widgets - Returns date, theme, modelName, modelCreator Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Simplify challenge schema: remove ChallengeEntry, use collections Based on architectural review feedback: - Remove ChallengeEntry model - entries are now CollectionItems - Make collectionId required on Challenge (auto-created on challenge creation) - Update ChallengeWinner unique constraint to allow ties - Add compound indexes for efficient feed queries - Update router to use CollectionItem counts - Update challenge detail page to link to collection for entries Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix SQL injection vulnerabilities and add visibility checks Security improvements based on agent review: - Replace $queryRawUnsafe with parameterized Prisma.sql queries - Add visibility check to getChallengeDetail (hide drafts and future challenges) - All user inputs now properly escaped via Prisma template literals Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Complete Phase 1 challenge platform implementation Schema changes: - modelVersionId -> modelVersionIds Int[] (array for OR logic) - Add allowedNsfwLevel Int (bitwise NSFW filter for entries) - Add entryPrizeRequirement Int - Make collectionId optional (auto-created) Router updates: - Auto-create Contest Mode collection on challenge creation - Hide Cancelled challenges from public - Include new fields in responses Job updates: - Auto-queue only queries Scheduled status (not Draft) - Entry validation uses ANY() for modelVersionIds - NSFW validation uses bitwise check against allowedNsfwLevel Form updates: - Add NSFW level selector with presets - Add entry prize requirement field Entry prize distribution: - New challenge-prize.ts with immediate award logic - Hook into collection.service.ts bulkSaveItems Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update challenge migration to match final schema - Remove ChallengeEntry table (using Collections instead) - Remove ChallengeEntryStatus enum - Add modelVersionIds array (replaces modelVersionId) - Add allowedNsfwLevel for bitwise NSFW filtering - Add entryPrizeRequirement field - Change ChallengeWinner unique constraint to userId (allows ties) - Add compound indexes for feed queries Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Remove modelId, use modelVersionIds array only - Remove modelId from Challenge table (use modelVersionIds instead) - Update queries to derive model name from first modelVersionId - Update filter from modelId to modelVersionId - Simplify form model selection (TODO: add proper version selector) - Clean up indexes in migration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add handoff documentation for Challenge Platform - Create docs/plans/challenge-platform-handoff.md with: - Quick start instructions for next developer/agent - Current status summary (Phase 1 backend complete) - What needs to be done (UI review, form improvements) - Key files reference table - Testing checklist - Environment requirements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Restore accidentally deleted docs * Add ModelVersionMultiSelect and ContentRatingSelect components for challenge form - Add ModelVersionMultiSelect component using resource select modal - Add ContentRatingSelect with visual badges and presets - Add getVersionsByIds endpoint for edit mode support - Fix schema alignment: use modelVersionIds array, add operationBudget - Clean up unused imports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update challenge platform docs to reflect completed form improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Refactor challenge platform with service layer and code cleanup - Extract business logic from challenge.router.ts into challenge.service.ts - Move types (ChallengeDetail, ChallengeListItem, etc.) to challenge.schema.ts - Fix cover image upload to create Image record via createImage() - Change "Manage Challenge" button to link directly to edit page - Remove unused imports and variables from challenge details page - Add collectionId guard in ChallengeEntries component - Update handoff documentation with architecture overview Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Refactor ChallengeUpsertForm and add mobile optimizations - Refactor ChallengeUpsertForm to use Form component pattern with custom Input components instead of Controller wrappers - Extend server upsertChallengeSchema for form validation - Update ModelVersionMultiSelect and ContentRatingSelect to use Input.Wrapper for form integration compatibility - Add mobile-responsive layouts using SimpleGrid and responsive props - Sync collection metadata when updating challenge dates/modelVersionIds Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update challenge platform handoff docs with latest changes - Updated status to 'Phase 1 Complete - Polished & Mobile-Optimized' - Added Form Refactoring section (completed) - Added Mobile Optimization section (completed) - Added Collection Metadata Sync section (completed) - Updated Architecture Overview with form architecture diagram - Added design decisions 9-11 (form schema extension, Input.Wrapper, collection sync) - Updated Key Files table with form library reference Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Cleanup imports in ChallengeCard, ModelSearchInput, and daily challenge files for cleaner code * feat(challenges): Move filters to SubNav and enhance challenge platform - Add ChallengeFeedFilters component with sort dropdown - Add ChallengeFiltersDropdown with status filter chips (Active/Upcoming/Completed/All) - Register challenge filters in SubNav following existing patterns - Remove inline filters from challenges index page - Enhance challenge card, upsert form, and detail page - Improve moderator challenge management UI - Update challenge service and schema Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(challenges): Add quick actions and simplify status lifecycle - Replace generic status dropdown with contextual quick actions: - "End & Pick Winners" for Active challenges - "Void Challenge" for Active/Scheduled challenges - Remove unused statuses (Draft, Judging) from ChallengeStatus enum - Simplify lifecycle: Scheduled → Active → Completed (or Cancelled) - Status transitions now controlled by dates and jobs, not manual input - Remove status input from ChallengeUpsertForm - Update Prisma schema, migration, and enums - Update documentation with implementation status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(challenges): Deprecate Redis tracking and enhance ChallengeInvitation Backend changes: - Challenge table is now primary source of truth for active/scheduled challenges - Deprecated setCurrentChallenge() - Challenge.status manages lifecycle - getCurrentChallenge() and getUpcomingChallenge() now query Challenge table first - Added adapter function for backward compatibility with legacy DailyChallengeDetails ChallengeInvitation updates: - Switched from dailyChallenge.getCurrent to challenge.getInfinite endpoint - Shows max 2 active challenges with "View all challenges" link - Displays creator profile picture using UserAvatar component - Shows username below end date in header - Navigation now uses /challenges/{id} instead of /articles/{articleId} Schema updates: - Added modelVersionIds, model, and collectionId to ChallengeListItem type - Updated service query to include new fields Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * chore: Update pnpm lockfile and regenerate Prisma types - Regenerated pnpm-lock.yaml after fresh install - Updated ChallengeStatus enum (removed Draft/Judging statuses) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add multi-challenge job processing support Changes from previous commit: **Multi-Challenge Processing** - reviewEntries() now processes ALL active challenges, not just one - pickWinners() handles multiple ended challenges and starts all ready scheduled challenges - Each challenge processed with error isolation (one failure doesn't stop others) - System challenges only auto-created when no upcoming system challenge exists **New Helper Functions (challenge-helpers.ts)** - getActiveChallengesFromDb() - returns all active challenges - getEndedActiveChallengesFromDb() - returns challenges past their endsAt - getScheduledChallengesReadyToStart() - returns scheduled challenges ready to activate - getUpcomingSystemChallengeFromDb() - checks for upcoming system challenges **Wrapper Functions (daily-challenge.utils.ts)** - getActiveChallenges(), getEndedActiveChallenges(), getChallengesReadyToStart() - getUpcomingSystemChallenge() - single function replacing redundant checks **Bug Fixes** - Fixed "Cannot read properties of undefined (reading 'reviewedAt')" error - Handle challenges without articleId (articleId: 0) throughout job processing - Added fallback to Challenge metadata for reviewedAt tracking - Use challengeId as fallback for notification keys when no articleId - Default lastReviewedAt to challenge start date instead of epoch **Documentation** - Updated challenge-platform.md with multi-challenge job processing details Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: redesign winners section UI and store completion summary Winners Section UI: - Podium-style layout (2nd | 1st elevated | 3rd) on desktop - Stacked layout on mobile with full-width cards - Gradient headers with place-specific styling (gold/silver/bronze) - Crown icon for 1st place winner - Expandable "Judge's Note" sections per winner - AI Judge Commentary section with judging process and final verdict - Clickable images linking to image detail page Challenge Completion: - Store AI-generated completion summary in Challenge.metadata.completionSummary - Display judging process and outcome on challenge detail page - Remove legacy Article creation/updates (no longer needed) UI Improvements: - Cover image height capped with object-fit: cover - Participation prize displays as blue buzz (not yellow) - Entry count shows only ACCEPTED entries Code Cleanup: - Remove unused SimpleGrid import - Remove test data setup actions from testing endpoint - Update to use getChallengeById instead of getChallengeDetails Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add developer testing guide for challenge platform Added comprehensive testing documentation including: - Testing endpoint actions reference - SQL queries for creating test challenges, collections, and entries - Instructions for multi-user entry setup (required for 3-winner testing) - Debugging queries for common issues - Test data cleanup queries Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: resolve type issue in daily-challenge.service.ts - Make articleId optional in ChallengeDetails type (deprecated) - Add challengeId field for new Challenge table support - Add deprecation notices to getCurrentDailyChallenge() and useQueryCurrentChallenge() - Fix ESLint warning for enum in template literal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address Copilot PR review comments for challenge platform - Fix SQL injection vulnerability in getAllChallengesFromDb by replacing $queryRawUnsafe with parameterized $queryRaw using Prisma.sql - Fix non-idempotent winner payouts by using stable userId in externalTransactionId instead of timestamp - Fix non-idempotent entry participation payouts by removing dateStr - Fix comment/code mismatch in getVisibleChallenges to exclude both Completed and Cancelled statuses as documented - Remove unused MediaType import from ChallengeCard.tsx - Remove unused dayjs import from challenge.service.ts - Update docs with correct ChallengeStatus enum values and file paths Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address remaining Copilot PR review comments - Add missing index on [status, visibleAt] in migration - Add externalTransactionId to immediate entry prize for idempotency - Remove fragile BuzzTransaction table query (rely on externalTransactionId) - Remove unused getChallengeById import from daily-challenge.utils.ts - Document pagination cursor limitation for non-Newest sorts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: implement composite cursor pagination for all challenge sorts - Add parseChallengeCursor and buildChallengeCursor helpers - Change cursor from number to string format "sortValue:id" - Implement keyset pagination for each sort type: - EndingSoon: endsAt ASC, id DESC - MostEntries: entryCount DESC, id DESC (with subquery comparison) - HighestPrize: prizePool DESC, id DESC - Newest: startsAt DESC, id DESC - Fix ChallengeStatus template literal warnings with String() - Use string literals for status enum in SQL to fix lint errors This ensures stable pagination across all sort types, preventing items from being skipped or duplicated between pages. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Small lint fixes * fix: use same externalTransactionId for entry prizes to prevent double payment The immediate entry prize (awarded when user reaches threshold) and end-of-challenge entry prize were using different externalTransactionId patterns, which could result in users receiving both prizes. Now both use `challenge-entry-prize-${challengeId}-${userId}` so the buzz service will deduplicate and only award once. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: unify externalTransactionId patterns to prevent double prize payments - Use stable challengeId-userId pattern instead of date-based IDs - Update descriptions to use challenge title instead of date/ID - Make challengeId required in DailyChallengeDetails type - Add title to ChallengeForPrize query for better descriptions Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add feature flag to enable/disable challenge platform Add a kill switch for the challenge platform via feature flags: - Add `challengePlatform` internal feature flag (env: FEATURE_FLAG_CHALLENGE_PLATFORM) - Add `CHALLENGE_PLATFORM_ENABLED` Flipt flag for server-side jobs - Hide challenges from navigation menu when disabled - Hide challenge indicator from generator panel when disabled - Protect all challenge API endpoints with isFlagProtected middleware - Block access to challenge pages (public and moderator) when disabled - Add early return guards to all challenge processing jobs When disabled, the platform becomes completely inaccessible: - Navigation: challenges tab hidden - Generator: trophy indicator hidden - Pages: return 404/NotFound - API: return FORBIDDEN error - Jobs: skip execution with log message Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Updates challenges feature flag permissions * refactor: format SQL queries for better readability in daily challenge processing * Enables challengePlatform feature flag * Add judge system, judgingPrompt override, UI/UX improvements, and router cleanup - Add ChallengeJudge model with system/review/winner prompts and migration - Add judgingPrompt textarea to ChallengeUpsertForm with judge persona auto-fill - Implement 3-tier prompt override chain: challenge prompt > judge persona > defaults - Update review and winner-picking jobs to fetch and pass judgingPrompt - Update cover image to 4:3 aspect ratio (form description + detail page display) - Consolidate getById/getByIdForEdit into single endpoint with moderator bypass - Patch @rajesh896/broprint.js exports for pnpm moduleResolution compatibility - Redesign challenge detail page layout with status badges and theme display - Add seed data for challenge judges and test challenges Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Add missing broprint.js patch file for pnpm moduleResolution compatibility The patchedDependencies entry was added in 9516eb0 but the actual patch file was never committed, causing pnpm install to fail with ENOENT. The patch adds a "types" condition to the package exports map so TypeScript bundler moduleResolution can locate the declaration file. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: enhance challenge detail page with eligible models, mod actions, and UX improvements - Replace single "Featured Model" with "Eligible Models" accordion showing all model versions with thumbnails, names, and per-model generate buttons - Add moderator context menu (dots icon) with edit, end & pick winners, void/cancel, and delete quick actions with confirmation dialogs - Show "Completed" badge in sidebar when challenge is finished - Constrain cover image width for better vertical space usage - Improve upsert schema with cross-field validation and active challenge guards - DRY cleanup: extract shared generator helper, mutation error handler, and accordion table props Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: complete challenge platform with entry submission, judge personas, and lifecycle jobs - Add entry submission modal (My Images, From Generator, Upload New tabs) - Add image eligibility checking (NSFW level, model version, recency) - Add challenge activation and completion background jobs - Add judge persona support (ChallengeJudge table, custom prompts) - Show judge as challenge creator in feed cards and detail page - Fix upsert bug where active challenge status was reset to Scheduled - Extract shared types (RecentEntry, SelectedResource) to challenge-helpers - Refactor endChallenge to delegate to closeChallengeCollection - Improve completed challenge sidebar indicator styling - Extract generator image download to shared utility - Update challenge platform docs, remove handoff doc - Add challenge router endpoints (eligibility, judges, quick actions) - Register all jobs in webhook runner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add date localization, refactor challenge cards, convert migration to webhook - Add DateLocaleProvider with browser locale detection for proper date formatting - Extend dayjs with localizedFormat plugin, use locale-aware format tokens - Refactor ChallengeCard to extract StatusBadge component, hoist shared styles - Convert challenge migration job to temp admin webhook endpoint with fixes: sets judgeId, normalizes prize format, updates collections to Contest mode, uses date-based status mapping - Set defaultJudgeId to CivBot, promote challengePlatform flag to public - Default challenge filters to active+upcoming, clean up dead code and TODOs - Remove seed-challenges.sql (no longer needed) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fixes dependencies issues * feat: improve cover image UI/UX and make it required Move cover image to a compact side column next to title/theme/invitation fields with responsive layout (stacks on mobile). Add 4:3 aspect ratio preview. Make coverImage required at both form and server schema levels. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add default judge config UI for system challenges - Add setChallengeConfig() Redis setter for updating challenge config - Add getSystemConfig/updateSystemConfig tRPC endpoints for moderators - Add SystemSettingsPopover in moderator challenges page header - Use optimistic updates for instant UI feedback on judge selection - Update ChallengeUpsertForm cover image to 4:3 aspect ratio - Clarify model version selection is OR condition in description - Improve ModelVersionMultiSelect UI with better empty state Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: enhance challenge judge system and client-side eligibility - Allow multiple judge configurations per user (remove unique constraint) - Add new judge prompt columns: collectionPrompt, contentPrompt, sourceCollectionId - Move entry eligibility validation to client-side for better UX - Add NSFW level badges to challenge submit modal - Include modelVersionIds in image data for client-side validation - Improve generator tab layout and NSFW level display - Refactor daily challenge utils and processing job Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: implement 30-day challenge auto-queue and update entry flow - Refactor createUpcomingChallenge() to accept optional targetDate param - Auto-queue job now creates challenges for all missing dates in 30-day horizon with sequential processing and 2s rate limiting between AI API calls - Add comprehensive logging throughout challenge creation pipeline - Add early validation for empty available users with descriptive errors - Update challenge description markdown to match current UI flow (Generate/Submit buttons on challenge page, 3-tab submission modal) - Remove unused collectionId from generateArticle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: optimize challenge auto-queue with batch processing and parallel AI calls Refactor challenge creation into a 3-phase batch architecture: 1. Pre-compute shared context once (3 parallel DB queries instead of 150 sequential) 2. Select resources sequentially with in-memory dedup to prevent duplicate models 3. Create challenges in parallel (concurrency 3, up to 6 concurrent AI calls) Also removes complete-review/complete-challenge actions from testing endpoint and switches AI model from GPT_5_NANO to GROK. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: migrate only the active daily challenge instead of all Narrow the migration script to only fetch the article-based challenge with status='active', since completed/old challenges don't need migrating. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: render challenge judging content as markdown instead of HTML Switch completionSummary.judgingProcess and outcome from RenderHtml to CustomMarkdown, and remove redundant "Judged by" text from winners header. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Updates pnpm-lock file * fix: challenge deletion with full cascade cleanup Replace entry-count guard with status-based guard (only block Active challenges) and cascade-delete the associated collection and its search index entry on challenge deletion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: default challenge allowedNsfwLevel to PG + PG-13 Replace hardcoded `allowedNsfwLevel: 1` (PG only) with `sfwBrowsingLevelsFlag` (PG + PG-13) so PG-13 entries are accepted. Update generated article rules text to dynamically display allowed levels instead of hardcoding "SFW (PG)". Also: add valueFormat to date pickers, refine getUpcomingSystemChallenge to only return scheduled challenges. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Defaults to sfwBrowsingLevel when creating new challenges * Updates feature flag to be granted only --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: manuelurenah <manuel.ureh@hotmail.com>
2026-02-06 15:28:21 -07:00
"@openrouter/sdk": "^0.5.1",
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.211.0",
feat(faro): wire bounded browser-trace sampling (pre-widening gate) (#2936) * feat(faro): wire bounded browser-trace sampling (pre-widening gate) Wire NEXT_PUBLIC_FARO_TRACES_SAMPLE_RATE (default 0.1) into the Faro browser tracer provider as a genuine OTel ParentBased(TraceIdRatioBased) sampler, so only ~10% of browser traces are recorded/exported while errors, web-vitals, events, and sessions stay at 100%. Stock @grafana/faro-web-tracing@2.8.2 COUPLES per-trace sampling to session sampling: its TracingInstrumentation hardcodes the WebTracerProvider sampler to getSamplingDecision(session), so a span is recorded iff the session is sampled. With sessionTracking.samplingRate at 1.0 that records ~100% of browser traces -- too much volume to widen RUM past the mod cohort, and there's no way to sub-sample traces via the session layer without also dropping errors/web-vitals. TracingInstrumentationOptions exposes no `sampler` field and the provider is built + globally registered inside a single private initialize(), so SampledTracingInstrumentation subclasses it and overrides initialize() with a faithful 1:1 copy of the 2.8.2 method, changing exactly one line: the WebTracerProvider `sampler` is our ratio sampler instead of faro's session-coupled default. Everything else (resource attributes, the FaroMetaAttributesSpanProcessor/BatchSpanProcessor/FaroTraceExporter chain, W3C propagator, default fetch/xhr instrumentations, initOTEL) is preserved. Pinned/commented to re-sync on any faro bump. Because only the tracer-provider sampler changes, session sampling stays 1.0 -> errors + web-vitals + events + sessions remain 100%; only OTel spans are sub-sampled. Edge cases: rate>=1 -> AlwaysOn, rate<=0 -> AlwaysOff (no traces, errors/vitals still flow), invalid/unset -> 0.1. Adds @opentelemetry/{sdk-trace-web,core,instrumentation} as direct deps (previously phantom via faro) pinned to faro's resolved versions so the override shares a single WebTracerProvider/propagator instance. DARK-safe: only changes browser trace volume, gated behind the existing `faro` flag + NEXT_PUBLIC_FARO_ENABLED; takes effect on next deploy. Tests: traceSampler.test.ts asserts the sampler is a genuine ParentBased(TraceIdRatioBased) at the configured ratio and -- the crux -- that trace sampling is independent of session sampling (traces=0 leaves session at 1.0; traces=0.1 does not set session to 0.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(faro): make decoupling test load-bearing + guard the fork/deps (audit follow-ups) Addresses the #2936 pre-merge audit (verdict: safe to merge, 3 follow-ups): A. FaroProvider now derives BOTH the session sampling rate and the trace sampler from `resolveFaroSampling` — the same helper the decoupling unit test asserts on. Previously FaroProvider called parseRate + createTraceSampler directly, so the "proves decoupling" test exercised a parallel path that couldn't catch drift in the real wiring (the same gap-hiding pattern that let an earlier redaction bug slip). The test is now load-bearing on the code path that actually runs in prod. B. Add a fork guard test asserting @grafana/faro-web-tracing is pinned at 2.8.2 (the version SampledTracingInstrumentation.initialize() was copied from) and SCHEDULED_BATCH_DELAY_MS === 1000. A future faro bump now FAILS LOUDLY, forcing whoever bumps it to diff upstream initialize() and re-sync the fork — the one failure mode the hard-fork design is exposed to. C. Pin @opentelemetry/resources to 2.9.0 to match faro's resolved version (sdk-trace-web/core/instrumentation were pinned but resources was left at the app's 2.5.0). Restores the "share faro's exact versions" invariant; interface-identical, zero new typecheck errors. Tests: 46 passed (14 traceSampler + 30 redact + 2 fork guard). typecheck: 0 errors on changed files; baseline error count unchanged (resources bump introduced none). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 08:59:19 -05:00
"@opentelemetry/core": "2.9.0",
"@opentelemetry/exporter-logs-otlp-proto": "^0.211.0",
"@opentelemetry/exporter-trace-otlp-proto": "^0.211.0",
feat(faro): wire bounded browser-trace sampling (pre-widening gate) (#2936) * feat(faro): wire bounded browser-trace sampling (pre-widening gate) Wire NEXT_PUBLIC_FARO_TRACES_SAMPLE_RATE (default 0.1) into the Faro browser tracer provider as a genuine OTel ParentBased(TraceIdRatioBased) sampler, so only ~10% of browser traces are recorded/exported while errors, web-vitals, events, and sessions stay at 100%. Stock @grafana/faro-web-tracing@2.8.2 COUPLES per-trace sampling to session sampling: its TracingInstrumentation hardcodes the WebTracerProvider sampler to getSamplingDecision(session), so a span is recorded iff the session is sampled. With sessionTracking.samplingRate at 1.0 that records ~100% of browser traces -- too much volume to widen RUM past the mod cohort, and there's no way to sub-sample traces via the session layer without also dropping errors/web-vitals. TracingInstrumentationOptions exposes no `sampler` field and the provider is built + globally registered inside a single private initialize(), so SampledTracingInstrumentation subclasses it and overrides initialize() with a faithful 1:1 copy of the 2.8.2 method, changing exactly one line: the WebTracerProvider `sampler` is our ratio sampler instead of faro's session-coupled default. Everything else (resource attributes, the FaroMetaAttributesSpanProcessor/BatchSpanProcessor/FaroTraceExporter chain, W3C propagator, default fetch/xhr instrumentations, initOTEL) is preserved. Pinned/commented to re-sync on any faro bump. Because only the tracer-provider sampler changes, session sampling stays 1.0 -> errors + web-vitals + events + sessions remain 100%; only OTel spans are sub-sampled. Edge cases: rate>=1 -> AlwaysOn, rate<=0 -> AlwaysOff (no traces, errors/vitals still flow), invalid/unset -> 0.1. Adds @opentelemetry/{sdk-trace-web,core,instrumentation} as direct deps (previously phantom via faro) pinned to faro's resolved versions so the override shares a single WebTracerProvider/propagator instance. DARK-safe: only changes browser trace volume, gated behind the existing `faro` flag + NEXT_PUBLIC_FARO_ENABLED; takes effect on next deploy. Tests: traceSampler.test.ts asserts the sampler is a genuine ParentBased(TraceIdRatioBased) at the configured ratio and -- the crux -- that trace sampling is independent of session sampling (traces=0 leaves session at 1.0; traces=0.1 does not set session to 0.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(faro): make decoupling test load-bearing + guard the fork/deps (audit follow-ups) Addresses the #2936 pre-merge audit (verdict: safe to merge, 3 follow-ups): A. FaroProvider now derives BOTH the session sampling rate and the trace sampler from `resolveFaroSampling` — the same helper the decoupling unit test asserts on. Previously FaroProvider called parseRate + createTraceSampler directly, so the "proves decoupling" test exercised a parallel path that couldn't catch drift in the real wiring (the same gap-hiding pattern that let an earlier redaction bug slip). The test is now load-bearing on the code path that actually runs in prod. B. Add a fork guard test asserting @grafana/faro-web-tracing is pinned at 2.8.2 (the version SampledTracingInstrumentation.initialize() was copied from) and SCHEDULED_BATCH_DELAY_MS === 1000. A future faro bump now FAILS LOUDLY, forcing whoever bumps it to diff upstream initialize() and re-sync the fork — the one failure mode the hard-fork design is exposed to. C. Pin @opentelemetry/resources to 2.9.0 to match faro's resolved version (sdk-trace-web/core/instrumentation were pinned but resources was left at the app's 2.5.0). Restores the "share faro's exact versions" invariant; interface-identical, zero new typecheck errors. Tests: 46 passed (14 traceSampler + 30 redact + 2 fork guard). typecheck: 0 errors on changed files; baseline error count unchanged (resources bump introduced none). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 08:59:19 -05:00
"@opentelemetry/instrumentation": "0.219.0",
"@opentelemetry/instrumentation-http": "^0.213.0",
"@opentelemetry/instrumentation-redis": "^0.61.0",
feat(faro): wire bounded browser-trace sampling (pre-widening gate) (#2936) * feat(faro): wire bounded browser-trace sampling (pre-widening gate) Wire NEXT_PUBLIC_FARO_TRACES_SAMPLE_RATE (default 0.1) into the Faro browser tracer provider as a genuine OTel ParentBased(TraceIdRatioBased) sampler, so only ~10% of browser traces are recorded/exported while errors, web-vitals, events, and sessions stay at 100%. Stock @grafana/faro-web-tracing@2.8.2 COUPLES per-trace sampling to session sampling: its TracingInstrumentation hardcodes the WebTracerProvider sampler to getSamplingDecision(session), so a span is recorded iff the session is sampled. With sessionTracking.samplingRate at 1.0 that records ~100% of browser traces -- too much volume to widen RUM past the mod cohort, and there's no way to sub-sample traces via the session layer without also dropping errors/web-vitals. TracingInstrumentationOptions exposes no `sampler` field and the provider is built + globally registered inside a single private initialize(), so SampledTracingInstrumentation subclasses it and overrides initialize() with a faithful 1:1 copy of the 2.8.2 method, changing exactly one line: the WebTracerProvider `sampler` is our ratio sampler instead of faro's session-coupled default. Everything else (resource attributes, the FaroMetaAttributesSpanProcessor/BatchSpanProcessor/FaroTraceExporter chain, W3C propagator, default fetch/xhr instrumentations, initOTEL) is preserved. Pinned/commented to re-sync on any faro bump. Because only the tracer-provider sampler changes, session sampling stays 1.0 -> errors + web-vitals + events + sessions remain 100%; only OTel spans are sub-sampled. Edge cases: rate>=1 -> AlwaysOn, rate<=0 -> AlwaysOff (no traces, errors/vitals still flow), invalid/unset -> 0.1. Adds @opentelemetry/{sdk-trace-web,core,instrumentation} as direct deps (previously phantom via faro) pinned to faro's resolved versions so the override shares a single WebTracerProvider/propagator instance. DARK-safe: only changes browser trace volume, gated behind the existing `faro` flag + NEXT_PUBLIC_FARO_ENABLED; takes effect on next deploy. Tests: traceSampler.test.ts asserts the sampler is a genuine ParentBased(TraceIdRatioBased) at the configured ratio and -- the crux -- that trace sampling is independent of session sampling (traces=0 leaves session at 1.0; traces=0.1 does not set session to 0.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(faro): make decoupling test load-bearing + guard the fork/deps (audit follow-ups) Addresses the #2936 pre-merge audit (verdict: safe to merge, 3 follow-ups): A. FaroProvider now derives BOTH the session sampling rate and the trace sampler from `resolveFaroSampling` — the same helper the decoupling unit test asserts on. Previously FaroProvider called parseRate + createTraceSampler directly, so the "proves decoupling" test exercised a parallel path that couldn't catch drift in the real wiring (the same gap-hiding pattern that let an earlier redaction bug slip). The test is now load-bearing on the code path that actually runs in prod. B. Add a fork guard test asserting @grafana/faro-web-tracing is pinned at 2.8.2 (the version SampledTracingInstrumentation.initialize() was copied from) and SCHEDULED_BATCH_DELAY_MS === 1000. A future faro bump now FAILS LOUDLY, forcing whoever bumps it to diff upstream initialize() and re-sync the fork — the one failure mode the hard-fork design is exposed to. C. Pin @opentelemetry/resources to 2.9.0 to match faro's resolved version (sdk-trace-web/core/instrumentation were pinned but resources was left at the app's 2.5.0). Restores the "share faro's exact versions" invariant; interface-identical, zero new typecheck errors. Tests: 46 passed (14 traceSampler + 30 redact + 2 fork guard). typecheck: 0 errors on changed files; baseline error count unchanged (resources bump introduced none). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 08:59:19 -05:00
"@opentelemetry/resources": "2.9.0",
"@opentelemetry/sdk-logs": "^0.211.0",
"@opentelemetry/sdk-node": "^0.211.0",
"@opentelemetry/sdk-trace-node": "^2.5.0",
feat(faro): wire bounded browser-trace sampling (pre-widening gate) (#2936) * feat(faro): wire bounded browser-trace sampling (pre-widening gate) Wire NEXT_PUBLIC_FARO_TRACES_SAMPLE_RATE (default 0.1) into the Faro browser tracer provider as a genuine OTel ParentBased(TraceIdRatioBased) sampler, so only ~10% of browser traces are recorded/exported while errors, web-vitals, events, and sessions stay at 100%. Stock @grafana/faro-web-tracing@2.8.2 COUPLES per-trace sampling to session sampling: its TracingInstrumentation hardcodes the WebTracerProvider sampler to getSamplingDecision(session), so a span is recorded iff the session is sampled. With sessionTracking.samplingRate at 1.0 that records ~100% of browser traces -- too much volume to widen RUM past the mod cohort, and there's no way to sub-sample traces via the session layer without also dropping errors/web-vitals. TracingInstrumentationOptions exposes no `sampler` field and the provider is built + globally registered inside a single private initialize(), so SampledTracingInstrumentation subclasses it and overrides initialize() with a faithful 1:1 copy of the 2.8.2 method, changing exactly one line: the WebTracerProvider `sampler` is our ratio sampler instead of faro's session-coupled default. Everything else (resource attributes, the FaroMetaAttributesSpanProcessor/BatchSpanProcessor/FaroTraceExporter chain, W3C propagator, default fetch/xhr instrumentations, initOTEL) is preserved. Pinned/commented to re-sync on any faro bump. Because only the tracer-provider sampler changes, session sampling stays 1.0 -> errors + web-vitals + events + sessions remain 100%; only OTel spans are sub-sampled. Edge cases: rate>=1 -> AlwaysOn, rate<=0 -> AlwaysOff (no traces, errors/vitals still flow), invalid/unset -> 0.1. Adds @opentelemetry/{sdk-trace-web,core,instrumentation} as direct deps (previously phantom via faro) pinned to faro's resolved versions so the override shares a single WebTracerProvider/propagator instance. DARK-safe: only changes browser trace volume, gated behind the existing `faro` flag + NEXT_PUBLIC_FARO_ENABLED; takes effect on next deploy. Tests: traceSampler.test.ts asserts the sampler is a genuine ParentBased(TraceIdRatioBased) at the configured ratio and -- the crux -- that trace sampling is independent of session sampling (traces=0 leaves session at 1.0; traces=0.1 does not set session to 0.1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(faro): make decoupling test load-bearing + guard the fork/deps (audit follow-ups) Addresses the #2936 pre-merge audit (verdict: safe to merge, 3 follow-ups): A. FaroProvider now derives BOTH the session sampling rate and the trace sampler from `resolveFaroSampling` — the same helper the decoupling unit test asserts on. Previously FaroProvider called parseRate + createTraceSampler directly, so the "proves decoupling" test exercised a parallel path that couldn't catch drift in the real wiring (the same gap-hiding pattern that let an earlier redaction bug slip). The test is now load-bearing on the code path that actually runs in prod. B. Add a fork guard test asserting @grafana/faro-web-tracing is pinned at 2.8.2 (the version SampledTracingInstrumentation.initialize() was copied from) and SCHEDULED_BATCH_DELAY_MS === 1000. A future faro bump now FAILS LOUDLY, forcing whoever bumps it to diff upstream initialize() and re-sync the fork — the one failure mode the hard-fork design is exposed to. C. Pin @opentelemetry/resources to 2.9.0 to match faro's resolved version (sdk-trace-web/core/instrumentation were pinned but resources was left at the app's 2.5.0). Restores the "share faro's exact versions" invariant; interface-identical, zero new typecheck errors. Tests: 46 passed (14 traceSampler + 30 redact + 2 fork guard). typecheck: 0 errors on changed files; baseline error count unchanged (resources bump introduced none). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 08:59:19 -05:00
"@opentelemetry/sdk-trace-web": "2.9.0",
"@opentelemetry/semantic-conventions": "^1.39.0",
2024-08-10 11:33:01 -04:00
"@paddle/paddle-js": "^1.2.1",
"@paddle/paddle-node-sdk": "^1.4.1",
2024-02-06 10:34:43 -04:00
"@paypal/react-paypal-js": "^8.1.3",
"@prisma/client": "^6.3.0",
"@prisma/instrumentation": "^7.4.2",
feat(profiling): Grafana Pyroscope continuous CPU profiling SDK (dark by default) (#3112) * feat(profiling): add Grafana Pyroscope continuous CPU profiling SDK (dark by default) Adds the @pyroscope/nodejs in-process wall/CPU profiler behind a PYROSCOPE_ENABLED env gate (default OFF). Ships as a complete no-op: the SDK is dynamically imported ONLY when the flag is armed, so an unset flag never loads the package or its native @datadog/pprof addon. When armed it runs a 100Hz wall+CPU sampler on a separate thread and pushes pprof to a configurable ingest endpoint (PYROSCOPE_SERVER_ADDRESS), tagged by pod/version/pool for diff-by-version flamegraphs. Purpose: continuous server-CPU profiling to rank and book per-request CPU optimizations (the live lever for our peak-capacity-capped web tier). - src/server/pyroscope.ts (new): flag-gated dynamic init, mirrors the cpu-profiler.ts arm-time-never-crashes discipline. Wall/CPU only (no heap), collectCpuTime on. - src/instrumentation.node.ts: fire-and-forget registration by the existing profiler hooks. - next.config.mjs: externalize the SDK + @datadog/pprof (native addon) from the server bundle. - package.json / pnpm-lock.yaml: add @pyroscope/nodejs@0.6.1 (exact pin — fresh native package). - .nvmrc: 20.13 -> 20.20.2 (the SDK's node engines floor). No behavior change with the flag unset (how it ships). * harden(pyroscope): make the profile flush interval env-tunable The flush (serialize -> protobuf encode -> push) runs on the MAIN thread every flushIntervalMs (default 60s), so on a CPU-ceiling-capped pool it's a periodic on-loop blip at peak. Wire PYROSCOPE_FLUSH_INTERVAL_MS so a Stage-2 canary can lengthen it from env (no code change) if event-loop-lag p99 shows the stall is material. Still dark by default; unset = SDK default 60s. (Addresses the sole arming-time risk from the #3112 adversarial audit.)
2026-07-14 10:07:00 -05:00
"@pyroscope/nodejs": "0.6.1",
2022-10-17 17:46:54 -06:00
"@react-hook/window-size": "^3.1.1",
2024-03-13 14:31:32 -04:00
"@react-pdf/renderer": "^3.3.8",
"@stripe/react-stripe-js": "^2.4.0",
"@stripe/stripe-js": "^2.2.0",
"@tabler/icons-react": "^3.7.0",
"@tanstack/react-query": "^5.101.0",
"@tanstack/react-query-devtools": "^5.101.0",
2025-08-01 11:22:37 -06:00
"@tanstack/react-virtual": "^3.13.12",
fix(perf): upgrade tiptap to 3.16.0 and happy-dom to 20.x (#2276) * fix(perf): upgrade tiptap to 3.16.0 and happy-dom to 20.x Resolves the SSR memory leak from unclosed happy-dom Window instances created by @tiptap/html/server's generateJSON. Upstream tiptap PR #6686 (shipped in @tiptap/html 3.6.4) fixed this by disposing the Window in a finally block; @tiptap/html 3.6.7+ requires happy-dom ^20 as a peer dep, which also closes CVE-2025-61927 (CVSS 9.4 VM context escape RCE). Supersedes #2275, which manually backported tiptap PR #6686 as a local wrapper. Pulling the actual upstream fix removes the wrapper debt and addresses the RCE in one shot. Tiptap pins ----------- @tiptap/core, /extensions, and /extension-text-style already floated to ^3.16.0; the other 15 packages were exact-pinned to 3.0.9. Production was running a mixed family with core at 3.16.0 against extensions at 3.0.9. Aligning all 18 to exact 3.16.0 narrows that gap rather than widening it. Suggestion plugin fix --------------------- @tiptap/suggestion 3.4.0 made two behavioral changes affecting src/components/RichTextEditor/suggestion.ts: 1. Escape onKeyDown: returning true now means "consumer handled the event, don't auto-close." The old code destroyed the component and returned true, which left the plugin's internal state (decorations, active flag) active. Removed the custom Escape branch — the plugin now handles Escape via onExit, which already does the cleanup. 2. Outside-click closing: the plugin no longer attaches a global document mousedown handler. Restored that behavior locally using the new exitSuggestion(view) export to dispatch the exit transaction. happy-dom 20 notes ------------------ happy-dom 20 disables JavaScript evaluation by default, which is desirable defense-in-depth for the article-content parse path (DOMParser.parseFromString doesn't execute scripts in the generateJSON flow regardless). happy-dom 19 dropped CommonJS support; Next.js 14 + Node 20 handles ESM-only packages via require(). Verified by a full next build run. Verification ------------ - pnpm install: lockfile clean, @tiptap/html@3.16.0 transitively resolves happy-dom@20.9.0 - pnpm exec tsc --noEmit: 4992 errors on main, 4992 errors on this branch — zero new errors anywhere (most are pre-existing Prisma type drift in article.service.ts and implicit-any drift elsewhere) - pnpm next lint src/components/RichTextEditor/suggestion.ts: only pre-existing any warnings on the untouched updatePosition() helper - next build: ✓ Compiled successfully (happy-dom 20 bundles cleanly through the Next.js loader; subsequent post-compile typecheck fails on the same AccountsCard.tsx error that fails on main) Manual smoke testing required pre-merge: article render path, comment @mention popup (open, arrow keys, Enter, Escape, outside click, selection of an item), paste a YouTube URL, paste a StrawPoll URL. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(rich-text): align mention suggestion to SuggestionPluginKey @tiptap/extension-mention's getSuggestionOptions creates a fresh anonymous `new PluginKey()` per suggestion instance (verified at node_modules/@tiptap/extension-mention/dist/index.js:14). Without overriding it, exitSuggestion() — which defaults to SuggestionPluginKey — would dispatch the exit metadata to a different key than the active plugin reducer was watching, so the outside-click handler would silently fail to close the popup and the document mousedown listener would never be cleaned up. The mention extension spreads overrideSuggestionOptions last (line 44 of the same file), so setting pluginKey in the suggestion config we pass to MentionNode.configure({ suggestion }) wins. Both the plugin and exitSuggestion() now reference the same key. Caught in review of #2276. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 08:23:17 -05:00
"@tiptap/core": "3.16.0",
"@tiptap/extension-color": "3.16.0",
"@tiptap/extension-heading": "3.16.0",
"@tiptap/extension-image": "3.16.0",
"@tiptap/extension-link": "3.16.0",
"@tiptap/extension-mention": "3.16.0",
"@tiptap/extension-placeholder": "3.16.0",
"@tiptap/extension-text": "3.16.0",
"@tiptap/extension-text-style": "3.16.0",
"@tiptap/extension-underline": "3.16.0",
"@tiptap/extension-youtube": "3.16.0",
"@tiptap/extensions": "3.16.0",
"@tiptap/html": "3.16.0",
"@tiptap/pm": "3.16.0",
"@tiptap/react": "3.16.0",
"@tiptap/starter-kit": "3.16.0",
"@tiptap/static-renderer": "3.16.0",
"@tiptap/suggestion": "3.16.0",
"@trpc/client": "^11.17.0",
"@trpc/next": "^11.17.0",
"@trpc/react-query": "^11.17.0",
"@trpc/server": "^11.17.0",
2024-03-13 14:36:08 -04:00
"@types/stream-to-blob": "^2.0.0",
"@typescript/analyze-trace": "^0.10.1",
2024-04-24 16:42:52 -06:00
"algoliasearch": "^4.23.3",
"archiver": "^6.0.1",
"blurhash": "^2.0.4",
2023-01-26 13:01:48 -07:00
"chalk": "^5.2.0",
"chart.js": "^4.4.0",
2023-12-01 17:42:44 -07:00
"chartjs-adapter-dayjs-4": "^1.0.4",
2025-05-29 16:27:30 -06:00
"circular-dependency-plugin": "^5.2.2",
2023-05-23 15:14:42 -06:00
"cloudflare": "^2.9.1",
"clsx": "^2.1.1",
2025-06-07 17:36:07 -04:00
"compromise": "^14.14.4",
"cookies-next": "^2.1.1",
"dayjs": "^1.11.12",
"decimal.js": "^10.5.0",
perf(trpc): Phase 1 — dual-format (union) decode, wire unchanged (#3125) First, additive step of a phased migration of the tRPC data transformer from superjson to devalue (devalue is ~2x faster to (de)serialize and produces a ~13-16% smaller wire payload, which reduces server-side serialize CPU and response size on the hottest code path). Phase 1 changes ZERO wire bytes. It only teaches every READER to decode BOTH formats; every WRITER still emits superjson. Because superjson.serialize always returns an object and devalue.stringify always returns a string, a format-sniffing union deserializer needs no negotiation: unionDeserialize(x) = typeof x === 'string' ? devalue.parse(x) : superjson.deserialize(x) Split into a tRPC v11 CombinedDataTransformer so READ and WRITE flip independently in later phases: - server: input.deserialize + output.deserialize = union; both serialize slots stay superjson (output.serialize keeps its serialize-timing wrapper). - client (4 links) + SSR helpers: output.deserialize = union; input.serialize stays superjson. The 3-phase plan: P1 (this PR): add union DECODE, keep writing superjson — no cross-version breakage is possible since the wire is unchanged. P2: flip WRITE to devalue behind a client-saturation gate; readers already accept both, so rollback is a one-line WRITE-only revert. P3: drop superjson + the union once devalue-only clients have drained. Also converts a raw Prisma Decimal `licensingFee` to a number in two currently-unwired model handlers, so they stay safe if ever re-attached (superjson silently string-coerces a Decimal; the phased devalue path would throw on it). Adds a pure unit test round-tripping Date, top-level BigInt, undefined fields, nested arrays/objects, and Map through the union for both writers. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:05:10 -05:00
"devalue": "5.8.1",
feat(app-blocks): line-level code diff in moderator review UI (#2831) The /apps/review ReviewModal previously showed only a FILE-level diff (added/changed/removed paths + counts) and a manifest field diff — a mod could see WHICH files changed but had to click out to Forgejo to read the actual code. This closes that "see exactly what changed" gap with an in-modal per-file unified line diff. Server: - computeBundleLineDiff: pure, IO-free per-file unified diff (via the `diff` lib's structuredPatch) between the pending bundle and the previous approved version. First version = whole-file adds. Hard bounds (the key correctness concern — never load unbounded content into memory/the response): TEXT FILES ONLY (binary by extension OR NUL-byte sniff is skipped), per-file 256 KiB byte cap, per-file 2000-line diff cap, and a 300-file total cap. Every elided file is explicitly marked (binary / too-large / diff-too-large / file-cap) so the UI shows the Forgejo fallback instead of silently dropping a change. - blocks.getPublishRequestDiff: moderator-gated tRPC query mirroring the auth/shape of getPublishRequestScreenshots (moderatorProcedure + isModerator belt + enforceAppBlocksFlag). Reuses the existing MinIO/Forgejo bundle-fetch helpers; fetches the previous approved bundle's bytes (excluding self) to diff against. UI: - ReviewModal gains a lazy "Show code diff" toggle under the file-diff list (query only fires when toggled). Each changed/added text file expands to a styled unified diff (+/- lines); elided files render a "view in Forgejo" fallback. Consistent with the existing review styling. Deps: promotes the already-transitively-pinned `diff@4.0.2` to a direct dependency + adds matching `@types/diff@4.0.2` (zero new resolution). Tests: 10 new unit tests for computeBundleLineDiff covering text-vs-binary detection (extension + NUL sniff), first-version all-add, a changed file's expected unified hunks, and every size/line/file-cap elision path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:28:47 -05:00
"diff": "4.0.2",
"discord-api-types": "^0.38.37",
2023-03-03 05:22:43 -07:00
"discord.js": "^14.7.1",
2024-10-30 17:39:37 -04:00
"dotenv": "^16.4.5",
2024-09-06 14:06:44 -04:00
"draft-js": "^0.11.7",
"embla-carousel": "^8.6.0",
2025-02-11 14:29:51 -07:00
"embla-carousel-autoplay": "^8.5.2",
"embla-carousel-react": "^8.5.2",
feat(scanner-policies): add moderator test bench for XGuard policy iteration Adds /moderator/scanner-policies, a moderator-only UI for authoring and scoring candidate XGuard policies against frozen test sets pulled from production moderator verdicts. Storage (no policies or results in the repo): - Candidates, system-prompt overrides, dataset records: sysRedis under REDIS_SYS_KEYS.SCANNER_POLICY (fail-open reads, fail-loud writes per system-cache.ts discipline) - Test workbooks + result merges: S3 (S3_UPLOAD_BUCKET, scanner-policies/ datasets/<mode>/<label>/...xlsx); one workbook per dataset, runs merge results into the same key in place Scoring (submit-and-callback, no synchronous wait): - startRun snapshots run state in sysRedis (candidates, rows, baseline, systemPrompt) and submits every (row × candidate) workflow with a callbackUrl pointing at /api/webhooks/scanner-policy-result. No wait, so the outer mutation returns in ms even for 2,500-call runs. - Webhook accumulates results in a sysRedis hash; counter increments atomically; finalizeRun fires when counter == total to build the xlsx (Results sheet merge by policyHash), upload back to S3, update the dataset's lastRun metadata, emit the terminal signal, and clean up. - Failed / cancelled / expired workflows still advance the counter so the run always finalizes; errors land in the Results sheet's errorMessage column. UI (Mantine v7 + tRPC): - Mode toggle (prompt / text), label sidebar, candidate editor with inline threshold + status edit - Per-mode system-prompt override panel (falls back to live xguard registry when unset) - Past datasets table with download / run / delete actions - Lazy SignalsProvider listener for progress (src/components/Signals/ ScannerPolicyTestSignal.ts) — attaches only when this page mounts Webhook + signals: - New SignalMessages.ScannerPolicyTestProgress - New /api/webhooks/scanner-policy-result with WEBHOOK_TOKEN guard + hExists idempotency check for re-delivered callbacks Dataset export: - Stratified sampling across TP/FP/TN/FN buckets from ScannerLabelReview joined with ScannerContentSnapshot (lower(label) match — DB stores lowercase while the registry is PascalCase) - Caps at user-specified max (default 500) with deterministic sort by contentHash so two exports of the same filter are byte-identical - Hidden _meta sheet preserves datasetId / mode / label across round trips Seed (one-time, idempotent): - scripts/seed-scanner-policies.ts populates the 12 prompt-mode + 15 text-mode live policies from xguard-manager export, plus the Young iterations explored this session (Options 4, 6, 11) Deps: + exceljs (server-side workbook io) Removed: docs/scanner-policies/, scripts/scanner-policy/ (CLI replaced by the in-app UI), .scanner-policy-cache/ gitignore entry
2026-06-01 18:22:23 -06:00
"exceljs": "^4.4.0",
"exifreader": "^4.39.0",
2025-06-03 16:31:42 -06:00
"fastest-levenshtein": "^1.0.16",
2024-02-19 10:07:13 -05:00
"file-saver": "^2.0.5",
"google-auth-library": "^9.15.0",
"googleapis": "^144.0.0",
2022-11-03 15:56:32 -06:00
"gray-matter": "^4.0.3",
2026-05-27 18:22:58 -06:00
"happy-dom": "^20.0.2",
2024-04-08 15:49:31 -06:00
"he": "^1.2.0",
2024-03-13 14:31:32 -04:00
"html-to-text": "^9.0.5",
2023-02-09 02:58:57 -07:00
"idb-keyval": "^6.2.0",
2022-10-19 17:03:43 -06:00
"immer": "^9.0.15",
2024-04-24 16:42:52 -06:00
"instantsearch.js": "4.64.1",
2025-06-12 13:22:05 -06:00
"jose": "^6.0.11",
"js-yaml": "^4.1.1",
2023-07-17 21:15:16 -06:00
"jsonwebtoken": "^9.0.1",
"jssha": "^3.3.1",
"jszip": "^3.10.1",
"konva": "^10.0.12",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash-es": "^4.17.21",
"lottie-react": "^2.4.1",
"lru-cache": "^11.2.2",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"mantine-react-table": "^2.0.0-beta.9",
2022-10-17 17:46:54 -06:00
"masonic": "^3.7.0",
2024-04-24 15:29:23 -06:00
"meilisearch": "0.33.0",
2024-11-18 16:22:18 -05:00
"motion": "^11.11.17",
2024-05-18 17:02:16 -06:00
"msgpackr": "^1.10.2",
chore(deps): upgrade to Next 16.3.0 and stop paying nested-async chunking in dev (#3742) * perf(dev): stop paying server nested-async chunking in dev The flag exists to shrink emitted server chunks in a production build. A dev server never emits those chunks, so the walk over dynamic-import paths is pure cost there, on a module graph `_app` already makes large. Keeps the build-time behaviour from #3458 unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(deps): upgrade next to 16.3.0 16.3.0 is the first release carrying vercel/next.js#93788, which fixes a lock-order inversion in turbo-tasks-backend and removes an incorrect `unsafe impl Send` on dash_map_multi::RefMut that let a caller hold a shard write guard across an await. The symptom is a dev server that pegs most of the cores with flat memory and never finishes compiling a route, recoverable only by restart. Verified by reading the file at each tag: the unsound impl is present through 16.2.12, the last 16.2 release, and gone in 16.3.0. It was never backported, so no 16.2.x can pick it up. The `get_multiple_mut` equal-keys assert still exists in 16.3.0, so this is not expected to silence that panic — only to stop a panicking worker from wedging every other worker behind it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs: commit the agent-rules block next dev writes into CLAUDE.md `next dev` on 16.3.0 appends a `nextjs-agent-rules` block to CLAUDE.md and re-adds it every run (next/dist/server/lib/generate-agent-files.js), so the choice is to commit it or carry an uncommitted change forever. It earns its place here: it points at `node_modules/next/dist/docs/`, which is already the authority this repo cites for Turbopack defaults, and the warning is aimed squarely at a pages-router app on Next 16. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 17:55:51 -06:00
"next": "^16.3.0",
2023-01-02 15:59:36 -07:00
"nodemailer": "^6.8.0",
"obscenity": "^0.4.5",
"openai": "^4.73.0",
2025-03-18 11:26:14 -04:00
"p-limit": "^6.2.0",
2023-05-12 11:18:56 -06:00
"path-to-regexp": "^6.2.1",
2024-02-01 22:23:36 -07:00
"pg": "^8.11.3",
2023-04-06 23:55:48 +01:00
"prom-client": "^14.2.0",
feat: simplified multi-chain crypto deposits with permanent addresses (#2105) * feat(crypto): simplified crypto deposit system with permanent addresses Replaces the old per-payment NowPayments flow with permanent deposit addresses per user. Adds deposit history with live signal updates, currency selector with fiat preference persistence, conversion rate display, and skeleton loading states. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): review pass — DRY, security, performance, accessibility DRY: Extract shared FIAT_OPTIONS, outerCardStyle, getFiatDisplay() into crypto-deposit.constants.ts. Create reusable FiatMenu component. Security: Remove webhook secret from error logs, add input bounds on perPage/page (max 25), cap concurrent API calls at 10. Performance: Replace spotlight setState with ref-based DOM manipulation, lazy-load QR code via next/dynamic, remove duplicate signal listener, stabilize callback refs, add staleTime to deposit history query. Backend: Fix broken reprocess-order endpoint, add division-by-zero guard in getBuzzConversionRate, replace `as never` Redis key casts with proper typing via paymentCacheKey helper. Accessibility: Add aria-labels to copy button, fiat menus, fee popover, and signal status refresh. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(crypto): multi-chain deposit addresses and custody sweep Add per-chain deposit address generation, chain-config registry, payout webhook handler, and custody sweep job for consolidating funds across chains. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): review pass 2 — DRY, hardening, performance, a11y DRY: Centralize chain display names and Buzz conversion formula in chain-config.ts. Remove duplicate maps from UI components. Hardening: Validate chain input with z.enum, add custody sweep idempotency via Redis dedup, fix auth token race condition with promise deduplication, sanitize webhook error responses, guard against NaN balance values. Performance: Stabilize onRetry and handleFiatChange callbacks to prevent unnecessary re-renders and API calls. Use ref pattern for updateSettings dependency. Accessibility: Wrap currency badges in UnstyledButton with aria-label and aria-pressed for keyboard and screen reader access. Tests: Sync test file with multi-chain service API changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): rename variant, add network labels, fix EVM display name - Rename DepositCardVariantC → DepositCardContent, remove variants/ dir - Fix EVM chain display name: "Base" → "Ethereum" - Show network in chain badge for multi-network chains (e.g., "Ethereum — Base") - Add NETWORK_DISPLAY_NAMES map and getNetworkDisplayName() to chain-config - Change "Min" → "Minimum" deposit label for translation compatibility - Update FiatMenu comment to reference new component name Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(crypto): replace API-based deposit history with local CryptoDeposit table - Add CryptoDeposit model (replaces CryptoDepositFee) with full deposit lifecycle tracking: status, amounts, fees, chain, timestamps - Migration drops CryptoDepositFee, creates CryptoDeposit with userId index - processDeposit now upserts CryptoDeposit on every webhook status - getDepositHistory queries local DB instead of NowPayments API — correct chronological ordering across all chains, no more API rate concerns - Remove bustDepositCache endpoint and payment status Redis caching - Remove CRYPTO_PAYMENT_STATUS and CRYPTO_DEPOSIT_HISTORY Redis keys - Add NOWPAYMENTS_IPN_URL env var for configurable webhook URL (dev support) - Simplify DepositHistory component: chain comes from DB, no currency lookup - Update tests to match new DB-based architecture (23 tests passing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): handle partially_paid, hide fees until finished, ceil rounding - Handle partially_paid webhook status (grants buzz like finished) — fixes $20 order amount causing partial payment instead of finished - Remove confirmed status handling (confusing UX, no action taken on it) - Only show fees in deposit history once status is finished - Ceil fee display to nearest cent instead of rounding Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): show proper ticker + network in deposit history Deposit history was showing raw NowPayments currency codes (e.g., "USDCBASE") instead of split ticker + network. Re-add currencies lookup (React Query deduplicates, no extra API calls) to resolve codes to proper tickers. Shows network badge only when it differs from the chain name (e.g., "USDC [Base]" but just "BTC" for Bitcoin). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): show network badge for multi-network tickers, fix fee on partial - Show network badge only when the ticker exists on multiple networks (e.g., "USDC [Base]", "USDT [Tron]") — single-network coins like BTC, DOGE, LTC show no badge - Fix fees not showing for partially_paid deposits (status check was only matching 'finished') Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): use dotted underline abbr-style for fee popover trigger Replace the info icon button with a dotted-underline text trigger on the fee amount itself. Clicking the fee text opens the popover with fee details. Uses cursor-help and decoration-dotted for the standard abbreviation affordance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor(crypto): centralize deposit completed status check Add isDepositComplete() to chain-config — single source of truth for statuses that mean buzz was credited (finished, partially_paid). Fixes toast notification not firing on partially_paid deposits. Replaces all scattered status === 'finished' checks across service, signal handler, and deposit history component. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(crypto): clarify deposit address and history are personal Add "Your" to deposit address label and recent deposits heading so users understand the address is tied to their account and sharing it would credit Buzz to them, not the recipient. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(crypto): add bonus buzz display, live timer, normalize deposit status - Add bonusBuzz (Int) and multiplier (Int, x100) columns to CryptoDeposit - Store membership multiplier and bonus amount at deposit time - Show bonus buzz via yellow + hover card with membership percentage - Normalize partially_paid to finished in DB and signals - Simplify downstream status checks to === 'finished' - Add live prop to DaysFromNow for auto-updating relative times - Use DaysFromNow in deposit history for live-ticking timestamps Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(membership): add collapsible benefits list with buzz multiplier highlight Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ui): align card styles, fix modal width, polish buzz purchase components - Switch Coinbase packageSection from Card to Paper with outerCardStyle to match Crypto tab card rendering (Card vs Paper have different defaults) - Add consistent box-shadow to inner cards (customAmountCard, bulkBenefitsCard, paymentSection) using the shared light-dark shadow pattern - Constrain BuzzPurchaseLayout Grid to max-width 1200px so the modal doesn't expand unconstrained (size="xxl" isn't a real Mantine preset) - Simplify BuzzFeatures: remove hover animations, use light variant icons - Simplify MembershipUpsell: remove collapsible benefits, float image, add gradient multiplier text, show all benefits inline - Fix BonusBuzzContent light mode contrast with Tailwind dark: variants - Bump currency selector badges from size sm to md, fix multi-network badge alignment by wrapping in UnstyledButton - Update BUZZ_FEATURE_LIST copy (generation types, cosmetic shop) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dashboard): consolidate buzz dashboard visual consistency Normalize card borders, gaps, titles, loading states, and chart styles across the entire buzz dashboard for a cohesive look and feel. - Add border to .tileCard SCSS class, fix CreatorProgramV2 light-mode bg - Normalize all grid gutters to md (16px), section spacing with mt-xl - Standardize card titles to text-xl font-bold (~20px) - Replace loading spinners with skeleton rows (PurchasedCodesCard) and contextual empty state (GenerationBuzzEmptyState) - Create GenerationBuzzEmptyState with split-panel gradient/spotlight design - Redesign GeneratedImagesRewards card: info popover, sectioned filter, abbreviated Y-axis ticks, thinned date labels, shared tooltip style - Extract shared chart defaults (scales, tooltips, legends) to chart-defaults.ts - Use Next.js Link for feature card buttons (client-side navigation) - Float clear-selection button over scroll area in Top Earning Resources - Fix Bank Buzz button height, RedeemCodeCard title color, code block light mode - Fix MembershipUpsell type errors (stale SCSS .d.ts, ReactNode benefits) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(dashboard): remove layout hacks and polish mobile view - Replace absolute positioning hack with proper CSS Grid for the Generation Buzz Earned card layout - Fix empty state collapsed to a thin line when no data - Remove arrow icon from transactions "View all" link to prevent title wrapping on mobile - Add whiteSpace: nowrap to View all link Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(ui): remove extra padding from empty deposit state, fix p-in-p nesting - Remove stacked py="md" from EmptyDepositState inner Stack (outer Paper p="lg" is sufficient) - Revert h="100%" from deposit card Papers (not needed in Stack layout) - Fix validateDOMNesting warning in MembershipUpsell BenefitRow (Text renders as div) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 16:13:34 -10:00
"qrcode.react": "^4.2.0",
2022-10-19 17:03:43 -06:00
"query-string": "^7.1.1",
2023-11-21 17:08:07 -07:00
"rand-seed": "^1.0.2",
2023-09-18 18:10:21 -04:00
"randomstring": "^1.3.0",
2024-09-16 21:12:07 -06:00
"react": "^18.3.1",
"react-blurhash": "^0.2.0",
"react-chartjs-2": "^5.2.0",
2024-09-16 21:12:07 -06:00
"react-dom": "^18.3.1",
2025-05-08 16:40:08 -06:00
"react-easy-crop": "^5.4.1",
2024-09-06 14:06:44 -04:00
"react-highlight-within-textarea": "^3.2.1",
"react-hook-form": "^7.71.1",
2024-07-05 17:16:39 -04:00
"react-instantsearch": "7.12.0",
"react-instantsearch-router-nextjs": "7.12.0",
"react-intersection-observer": "^9.4.0",
"react-joyride": "^2.9.3",
"react-konva": "^18.2.14",
"react-markdown": "^9.0.1",
2024-02-16 11:02:00 -04:00
"react-social-media-embed": "^2.5.9",
fix(blocks): reject ReDoS-prone manifest patterns + bound regex input (#3343) * fix(blocks): reject ReDoS-prone manifest patterns + bound regex input (settings-validator) App Blocks string-settings can declare a `pattern` (regex) in the manifest. `validateBlockSettings` runs `new RegExp(def.pattern).test(raw)` where the pattern is app-developer-authored (mod-reviewed only) and `raw` is viewer-supplied. Submission validation previously only checked the pattern *compiles* — but a compilable regex can still be catastrophically exponential (`(a+)+$`, `(x+x+)+y`, `(.*)*` freeze on ~40 chars), so a published app could arm a ReDoS that freezes a pod's event loop on any viewer save. Same class as the just-fixed og-metadata O(n^2) and comfy triggerWord exponential bugs; MED (app-dev-controlled + mod-gated => lower likelihood). Defense in depth, no native dependency: 1. Submission gate: reject super-linear patterns via `safe-regex` (pure-JS, flags nested/unbounded quantifiers / star height > 1). Beyond "does it compile". 2. Submission gate: a field declaring a `pattern` must also declare `max_length`, hard-capped at MAX_PATTERNED_INPUT_LEN (1000). 3. Eval site: hard-cap the length of `raw` the regex is ever run on, regardless of whether max_length was declared (belt for patterns stored before this gate). Fully tames polynomial patterns; the submission gate is the primary defense against exponential ones. safe-regex (+ @types/safe-regex) added to package.json; it pulls pure-JS regexp-tree — no re2 / native addon. Extends the existing unit tests for both files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: update pnpm-lock.yaml for safe-regex direct dep package.json declared safe-regex + @types/safe-regex as direct deps (the ReDoS gate) but the lockfile importers section wasn't regenerated, so CI's --frozen-lockfile install would fail. Regenerated with pnpm@10.28.1 --lockfile-only (minimal 24-line diff; safe-regex@2.1.1 was already resolved transitively). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): rework manifest-pattern ReDoS gate — move to submission gate + accurate recheck, fix fail-open Reworks the App Blocks settings-pattern ReDoS defense after an adversarial audit found the prior approach was misplaced and over-aggressive. What was wrong (PR #3343 v1): - safe-regex FALSE-POSITIVES: its coarse star-height heuristic rejected common LINEAR patterns (slug `^[a-z0-9]+(-[a-z0-9]+)*$`, decimal, snake_case) that are not ReDoS. The rejection was silent (settings form rendered zero fields). - Wrong layer: the gate lived in manifest-settings.meta.schema (install/display time), NOT at manifest submission — so a bad pattern was still stored and only a fragile implicit fail-open protected runtime. - Fail-open widened: install paths do `parsed.success ? validate(...) : rawInput`, so rejecting more patterns in the meta-schema silently SKIPPED all field (type/enum/range) validation for existing manifests. Reworked design: - Move the ReDoS + `max_length`-required gate to the REAL submission gate. New `BlockManifestValidator.validateSubmission` runs the sync `validate` plus an accurate ReDoS check on settings-field patterns, wired into all four submission paths (git-push webhook, developer manifest API, blocks.updateManifest, publish-request approve). "A stored/approved manifest ⇒ non-exponential, input-bounded patterns" is now an ENFORCED invariant with real dev feedback. - Replace safe-regex with `recheck` (accurate automaton+fuzzing ReDoS analysis). Verified: slug/decimal/snake PASS (safe); (a+)+$, (x+x+)+y, ([a-zA-Z]+)*$ are REJECTED (vulnerable). recheck is forced to its portable in-process `pure` engine (its native binary can't run on some hosts and hangs) in a SERVER-ONLY module reached via dynamic import, so recheck never enters the client bundle and `validate` stays synchronous/client-safe. - Fix the fail-open: manifest-settings.meta.schema reverts to compile-check-only, so existing manifests (incl. slug patterns) parse successfully again and field validation runs instead of being silently skipped. - Keep the eval-site input cap (settings-validator: raw <= 1000 before .test()) — defense-in-depth with no false-positive risk. - Regenerate pnpm-lock.yaml (drop safe-regex/@types/safe-regex, add recheck) with pnpm@10.28.1; passes --frozen-lockfile. Existing installs are not retroactively hard-failed: the submission gate applies to new/re-submitted manifests; the removed fail-open + kept input cap cover the runtime for any legacy pattern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): fail-closed on recheck 'unknown' in the manifest ReDoS gate isPatternRedosVulnerable returned true only for status==='vulnerable', so a recheck 'unknown' verdict (analysis error / timeout) was treated as safe and ACCEPTED — a bypass: a pattern crafted to stall recheck's own analysis would sail through the submission gate and reach the eval-time .test(), and an exponential pattern freezes even on the max_length-bounded input. Reject anything not definitively 'safe' (status !== 'safe'). Adds an isolated fail-closed test (mocks recheck -> 'unknown' -> asserts rejected). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 15:32:40 -05:00
"recheck": "^4.5.0",
"redis": "^5.8.3",
"rehype-raw": "^7.0.0",
"rehype-stringify": "^10.0.1",
"remark-breaks": "^4.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.1",
"request-ip": "^3.3.0",
2025-06-09 13:05:39 -04:00
"sanitize-html": "2.12.1",
2024-12-05 10:26:06 -07:00
"sass": "^1.82.0",
2024-03-14 21:50:04 -06:00
"semver": "^7.6.0",
"sharp": "^0.32.6",
"slate": "^0.94.1",
"slate-history": "^0.93.0",
"slate-react": "^0.95.0",
"slugify": "^1.6.5",
2023-02-03 02:53:56 -07:00
"socket.io-client": "^4.5.4",
2024-12-20 13:33:36 -07:00
"source-map": "^0.7.4",
"stacktrace-parser": "^0.1.10",
2024-03-13 14:31:32 -04:00
"stream-to-blob": "^2.0.1",
2023-01-18 12:16:40 -07:00
"stripe": "^11.6.0",
perf(trpc): upgrade superjson 1.9.1 → 2.2.6 (#2425) Replaces the closed devalue migration (#2410) with the lower-risk lever from the best-practice audit. We were 3 major versions behind (1.9.1 → 2.2.6, the latter actively maintained), and every benchmark that motivated the devalue swap compared devalue only against superjson 1.x — the stale version we run. superjson 2.x is the era after its serialization-perf work, so this directly tests "is the serializer the cost?" with none of devalue's downsides. Why this over devalue: - LENIENT, like 1.x — still coerces Prisma Decimal / dayjs / SDK objects / Error / class instances instead of throwing. No per-route 500-risk tax, no SSR full-page-500 surface (the recurring problem with devalue). - Drop-in: identical `{json, meta, v:1}` wire format (verified) → cross-version compatible during the rolling deploy, and the existing transformer wiring (`superjson.serialize`/`.deserialize`, default import at 3 sites) is unchanged. - ESM-only is superjson 2.x's only breaking change (Node ≥16) — handled by adding `superjson` to `transpilePackages` for the standalone server bundle (same mechanism devalue needed). No `require()` of superjson exists in src/. Verified v2: default export intact, serialize/deserialize round-trip Date/Map. Transitive deps copy-anything 3→4, is-what 4→5 are superjson's own. The higher-ceiling fix (epoch dates + no transformer on the feed path) remains a separate follow-up. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 19:25:59 -05:00
"superjson": "^2.2.6",
"three": "^0.180.0",
2023-04-10 18:15:45 -06:00
"trie-memoize": "^1.2.0",
"unfurl.js": "^6.4.0",
"unified": "^11.0.5",
Knights of New Order (#1675) * Initial changes for knights of new order game * Updates endpoints, jobs and signals for kono * Refactors NewOrderPlayer handling and updates related schemas and signal topics * Add method to get a set of images for a certain user * Type checking, update code to use rankType * Add images to queue during a scan job * Renames Acolyte properly * Add exists * Implement Knights New Order game features: player joining logic, and server-side session checks * Add new test endpoint for queues * Includes migration * Process Knight queue images * Increase value in pool * Clear image from queue * nits & fixes - queues * Cleanup * Fetches images queue from UI * Refactor Knights New Order game logic and UI components; implement image rating system with new enums and state management. * Initial changes to display judgement history * Bunch of updates to make things better * Updates welcome screen * update grant blessed buzz job with simplier logic * Update how we handle daily resets to be more efficient and accurate on the DB * Update more jobs and how they work * Several UI updates * Add new cache for ranked images * Adjustments for mobile support * Ensure we bust cache to avoid same images * Add proper way to handle the ratings with a better coded counter * add Templar logic to addImageRating * Completes support for mobile devices * Includes leaderboard changes * Adds raters and inquisitor adjustments * Removes isModerator when requesting image queue * Adds smite action for mods * Adds player directory * More adjustments to inquisitor tools * Fixes layout issues based on feedback * Clean up and apply feedback * Major updates to queues * Fixes type issues * Fixes new order jobs * Removes comment from migration * Cleanup * Adds notifications and user menu item for kono * Adjusts notifications * Makes the newOrder game public * Fixes all around * More feedback adjustments * Fixes issue when trying to update the image nsfw level * More fixes when updating image nsfwLevel * Refactor image rating logic to update NSFW level for moderators and streamline queue handling * Adjusts exp gained when rating images * More adjustments for better understanding * Hotfix to avoid getting duplicated images while reviewing * Includes content warning modal before joining * Fixes shuffling * Fixes type issues * Quick adjustments based on feedback * Add acolyte smite system * Fix type * Use redis EXP instead of db exp * Nits & cleanup * Adds beta banner * Ensure we cleanse smites as pending image ratings get resolved * Fixes issues when rating as mod * Plays failed sound is rating does not match * Allows mods to reset a player career * Includes rank icons and avoids getting unpublished images * Allows claiming cosmetics when joining and ranking up * Updates welcome screen * Restricts access to mod only --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com>
2025-05-09 11:46:03 -04:00
"use-sound": "^5.0.0",
"uuid": "^9.0.0",
"viem": "^2.30.6",
2023-12-13 10:42:29 -07:00
"xml2js": "^0.6.2",
feat: App Blocks v1 — block-host substrate, CORS, JWT, publisher-install (model.sidebar_top slot) (#2319) * feat(blocks): App Blocks v1 — substrate, JWT, manifest registry, model.sidebar_top Implements App Blocks v1: a substrate for rendering third-party iframe-embedded blocks on civitai model pages, authenticated via short-lived RS256 JWTs scoped to individual block installs. Architecture: docs/features/app-blocks.md (new). DATABASE (prisma/migrations/20260524120000_app_blocks_initial): - app_blocks: registry; status, trust_tier, render_mode, approved_scopes[], v2 substrate columns (asset_bundle_*) - model_block_installs: per-(model, slot) install rows; composite UNIQUE (model_id, app_block_id, slot_id); installed_by SET NULL on user delete; FK indexes for delete pipelines; slot_id CHECK; TEXT PK length CHECKs - block_user_settings: per-(viewer, instance); CASCADE on install + GDPR user delete - platform_default_blocks: moderator-promoted defaults; partial index (slot_id, priority) WHERE enabled; SET NULL on promoter delete tRPC blocks router (src/server/routers/blocks.router.ts): - listForModel: public, slot enum-validated, flag-gated [] when off, threads modelType + modelNsfwLevel for content-rating filter - installOnModel / updateSettings / toggleEnabled / uninstallFromModel: guardedProcedure (verified + non-muted); dbWrite for auth lookups; updateSettings pins modelId in WHERE; install cap enforced at insert (rejects 4th); byte-length 4KB cap withBlockScope middleware (src/server/middleware/block-scope.middleware.ts): - RS256-only JWT verify; kid-based key select with NEXT-rotation fallback; clockTolerance: 30s, maxTokenAge: 15m; scalar assertions on iat/exp/jti/aud - Per-scope context binding: models:read:self → query.id integer-match; media/buzz/social/user:read:self → non-anon sub; ai:write:budgeted → positive buzzBudget; block:settings:* → blockInstanceId match (decimal-only modelId parse; array-form query rejected) - Deny-by-default for unknown scopes - Per-instance revocation check (Redis marker) - CORS/cache isolation: wraps res.setHeader to prevent wrapped PublicEndpoint/AuthedEndpoint from clobbering; forces Cache-Control: private, no-store on block-JWT responses API endpoints: - POST /api/v1/block-tokens: same-origin EXACT host match (rejects POST without Origin + non-allowlisted Origin with 403); per-IP rate limit with in-process LRU fallback; CF-Connecting-IP only when cf-ray present; per-(user/ip, instance) rate limit BEFORE DB lookup; ban/mute/deleted gate at issuance; OAuth-bit scope allowlist + approved_scopes snapshot intersection; settings tokens require caller==installer + 5-min TTL; client slotContext allowlist + scalar coerce; server stamps modelId + slotId; Flipt-gated 503 - GET /api/v1/block-tokens/jwks: 60s cache + ETag; 503 when not configured or malformed; flag-gated - GET /api/v1/blocks/me: user:read:self; banned rejected; dbWrite - POST /api/v1/developer/block-manifests: JOB_TOKEN timingSafeEqual; 64KB bodyParser cap + byte-length 32KB manifest cap; trustTier/renderMode FORCED to unverified/iframe on INSERT (admin promotion is a separate Phase 2 path); UPDATE resets status='pending' + 403s on tier change; byte-equal no-op short-circuit; flag-gated - POST /api/internal/blocks/workflow-completed: JOB_TOKEN; Redis- backed workflowId idempotency (7-day TTL, fail-closed); flag-gated V1 route wrapping: - /api/v1/models/[id] wrapped with withBlockScope (models:read:self) - /api/v1/me unchanged; App Blocks use the dedicated /api/v1/blocks/me Manifest validator (src/server/services/block-manifest-validator.service.ts): - Trust-tier-gated sandbox token allowlist - SSRF gate: rejects RFC1918, loopback, link-local, IPv6 ULA fc00::/7, zone identifiers, .internal/.local/metadata.*, dotted + dotless hex/octal/integer IPv4, IPv4-mapped IPv6 - Manifest URLs bound to OauthClient.allowedOrigins (H8) - Iframe height envelope; sandbox non-empty; publicSettingsKeys allowlist for listForModel exposure React tree (src/components/AppBlocks/): - BlockSlot: Flipt-gated mount; keyed on (slotId, modelId) so navigation force-unmounts; renders nothing when no installs - BlockHost + BlockErrorBoundary: error containment - IframeHost: full BLOCK_INIT → BLOCK_READY → RESIZE_IFRAME lifecycle; iframe-loaded as state so 10s timeout arms after token-late-load; 15s token-wait timeout; empty-src early fail; hard 8000px height ceiling + isFinite guards; referrerPolicy=no-referrer, loading=lazy; client-side sandbox intersection; RESIZE gated on ready; TOKEN_REFRESH postMessage on token rotation (no remount) - useBlockToken: AbortController per request; absolute refreshAtRef drives visibility-resume; document-hidden pause; jittered 429 backoff; doesn't set pending=true on refresh - usePostMessage: origin match + event.source window-identity pin; rate limit + LRU dedup Feature flag (Flipt key app-blocks-enabled): Gates every server-side surface AND the BlockSlot mount. Off by default; flag is the launch lever + kill switch. Tests (vitest): - block-scope constants - manifest validator (sandbox + SSRF + binding + size + content rating) - context binding + JWT classics (alg=none, HS256 confusion, expired, wrong iss/aud) - block-registry SQL invariants + install cap + content rating + publisher settings projection + toggleEnabled revocation cycle - block-token handler (CSRF reject, flag-off, banned/deleted, settings ownership, scope allowlist, ctx coercion) - manifest registration (status reset + trust-tier lockdown) Pre-launch checklist (deploy-side): - BLOCK_TOKEN_PRIVATE_KEY + BLOCK_TOKEN_PUBLIC_KEY set - BLOCK_ALLOWED_ORIGINS includes prod blocks origin - blocks.civitai.com without X-Frame-Options: DENY - Flipt app-blocks-enabled flag toggled on - CF-only ingress on civitai-main (per-IP rate limit depends on it) Phase 2/3 deferred (documented in PR description): publisher install UX, admin tier-change tool, moderator approval UI, audit log table, ClickHouse telemetry, health-check + auto-suspend, per-jti revocation denylist, per-app OAuth (replacing JOB_TOKEN), DNS-rebinding gate at fetch time, CSP frame-src on model pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(app-blocks): unblock iframe load + give trusted blocks real origin Two issues prevented BlockHost from ever showing the iframe in PR-2319: 1. loading="lazy" + initial display:none deadlocked the load. With the iframe out of layout it's never "near viewport", so the lazy gate never fires, onLoad never runs, status never transitions to ready, and display stays none. Drop loading="lazy" so the iframe loads on mount; size + visibility are still controlled via inline style. 2. The client-side sandbox allowlist strips allow-same-origin unconditionally, but the rest of the messaging design assumes a real iframe origin: usePostMessage.send uses an explicit targetOrigin = new URL(iframe.src).origin and usePostMessage's inbound listener gates on event.origin === expectedOrigin. An opaque-origin iframe ("null") never matches either side, and its subresources also fail CORS at the static host. Permit allow-same-origin for trusted tiers (internal, verified) so this works as designed. Unverified blocks still get an opaque origin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(blocks): export verifyBlockToken for tRPC reuse Workflow procedures in blocks.router need the same JWT verification gate as the Next.js API middleware. Exporting the existing helper keeps signer/verifier behavior in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): wire workflow procedures (poll/estimate/submit) Adds the host↔orchestrator bridge the App Blocks SDK expects. Blocks can now drive useBuzzWorkflow().{estimate,submit,poll} end-to-end: - pollWorkflow: read status via the user's orchestrator token (orchestrator enforces ownership server-side) - estimateWorkflow: cost preview via submit + whatif=true; no budget gate - submitWorkflow: budget gate via cost preflight, anon rejection, prompt audit before any orchestrator call. Over-budget returns a failed-shape snapshot rather than throwing — the SDK treats throws as block lifecycle errors but expects budget rejections as recoverable outcomes. All three verify the block JWT via the shared verifyBlockToken helper and re-check context binding (claims.ctx.modelId === input.body.modelId) plus the modelVersionId → modelId DB chain. Workflow body schema is a strict discriminated union (textToImage only for v1). Server fills baseModel from the version row and conservative defaults (sampler=Euler, steps=25, dimensions per base-model family) so blocks don't need to know platform-side gen params. Tags every block-submitted workflow with app-block:{appId,block,instance} for billing attribution and post-incident review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): host workflow + buzz-purchase postMessage bridge Subscribes IframeHost to the four block→host messages the SDK now expects: - SUBMIT_WORKFLOW → trpc.blocks.submitWorkflow → WORKFLOW_SUBMITTED - ESTIMATE_WORKFLOW → trpc.blocks.estimateWorkflow → ESTIMATE_RESULT - POLL_WORKFLOW → trpc.blocks.pollWorkflow → WORKFLOW_STATUS - OPEN_BUZZ_PURCHASE → BuyBuzzModal → BUZZ_PURCHASE_RESULT Every handler validates requestId is a string (drop otherwise) and echoes it back verbatim — the SDK's sendTypedRequest correlates by id and times out after 30s if we never reply. tRPC errors are converted to failure-shape snapshots rather than thrown, so the block can render "top up Buzz" CTAs instead of seeing a lifecycle error. OPEN_BUZZ_PURCHASE caps the attacker-controlled `suggestedAmount` at 50k buzz before pre-filling the modal, and uses a per-requestId dialog id so overlapping requests don't collapse in the dialog store dedup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(blocks): cover workflow service helpers + router gates 32 tests across two files: - workflow.service.test.ts: snapshotFromWorkflow status mapping, image-url filtering (drops pending/empty/blocked), version resolver's not-found vs forbidden gates, buildTextToImageInput defaults per base-model family. - blocks.router.workflow.test.ts: every security gate on each procedure (invalid token, missing scope, modelId mismatch, version belongs to different model, anon submit, over-budget, prompt-audit fails closed, flag disabled, malformed body). Asserts the cost preflight + real submit are wired in the right order and that whatif is set on the estimate path. IframeHost handler tests are deferred — this repo has no React component test infrastructure (vitest runs in node env, no jsdom or testing-library setup). Adding that is its own task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): drop tier from User select — not a Prisma column getBlockSessionUser was selecting `tier: true` from the User table, which Prisma rejects: tier isn't on User. It's derived from active subscriptions and stamped on SessionUser at session-creation time (see types/next-auth.d.ts). Block-initiated calls fall through to free-tier limits via the `user?.tier ?? 'free'` default the orchestrator helpers already apply. Higher-tier users get free-tier limits when generating through a block — acceptable for v1; if blocks need parity with web generation we'll mirror the session tier-resolution logic in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): prepend platform checkpoint for non-Checkpoint models The orchestrator rejects workflows with no Checkpoint in resources — the run needs an anchor model. Blocks bound to a LoRA were sending just `[{ id: loraVersionId }]`, hitting "A checkpoint is required to make a generation request" at parseGenerateImageInput. When the bound model isn't itself a Checkpoint, prepend the platform's per-family default. v1 wires Flux1 only (version 691639, the fluxStandardAir canonical checkpoint). Other base-model families return BAD_REQUEST with a clear message until product picks canonical checkpoints for them — that's a buzz-attribution + UX decision, not something we should hardcode silently. resolveBlockVersionContext already returns modelType; widening the buildTextToImageInput signature is the only change at the call sites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): real checkpoint-selector chain (deletes band-aid map) Replaces the hardcoded DEFAULT_CHECKPOINT_VERSION_BY_FAMILY={Flux1: 691639} map with a real per-install / per-viewer selector. Server-side only — the SDK + block-app changes ride in a follow-up. Data lives in two existing JSONB columns; no migration: - model_block_installs.settings.default_checkpoint_version_id (publisher) - block_user_settings.settings.checkpoint_version_id (viewer) Precedence chain at submit time (checkpoint.service.ts): 1. Bound model is a Checkpoint → it's its own anchor (skip overrides) 2. Viewer override (re-validated; drop-on-invalid → fall through) 3. Publisher default (re-validated; throws on invalid so author sees it) 4. BAD_REQUEST — no platform fallback. Install is misconfigured. Validation distinguishes not-found / not-published / not-a-checkpoint / wrong-ecosystem via TRPCError.cause.reason so the install-form UI can render inline errors. New surface: - src/server/schema/blocks/settings.schema.ts: per-block-id typed shapes - src/server/services/blocks/checkpoint.service.ts: validateBlockCheckpoint, getRepresentativeBaseModel, resolveBlockCheckpoint - blocks.updateUserSettings tRPC (block JWT-gated, host-mediated) - blocks.getEffectiveCheckpoint tRPC (publisher ∪ viewer merge for BLOCK_INIT) - BlockRegistry.upsertUserSettings / getUserSettings / getEffectiveCheckpoint - BlockInstallRecord.defaultCheckpoint (anon-safe — viewer override is delivered separately through the new query) - BlockRegistry.listForModel: batched ModelVersion join populates defaultCheckpoint without N+1 - buildTextToImageInput: dropped the family map, now takes an explicit checkpointVersionId from the router after resolveBlockCheckpoint Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): host picker handlers + context.checkpoint merge Three IframeHost changes: 1. BLOCK_INIT.context.checkpoint — IframeHost now fetches the effective (publisher-default ∪ viewer-override) checkpoint via the new blocks.getEffectiveCheckpoint query and merges it into the init payload BEFORE sending. Init waits for the query to land so the block never sees a stale value and re-mount. 2. OPEN_CHECKPOINT_PICKER → opens the platform's existing openResourceSelectModal filtered to Checkpoints in the requested ecosystem (baseModelGroup expanded via getBaseModelsByGroup). Posts CHECKPOINT_PICKER_RESULT with the selection, or an empty result on dismiss. Guards against double-emission via an `answered` latch since the modal calls onSelect THEN onClose on successful pick. 3. SET_USER_CHECKPOINT → calls trpc.blocks.updateUserSettings with the block token, refetches getEffectiveCheckpoint so a subsequent BLOCK_INIT reflects the new value, posts USER_CHECKPOINT_SET with ok/error shape. ModelSlotContext type extended with the optional `checkpoint` field (BlockCheckpointInfo). null when no checkpoint configured AND model isn't itself one — block renders a "missing checkpoint" state in that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): debug endpoint for setting install defaults Standalone WEBHOOK_TOKEN-gated endpoint at /api/testing/blocks for setting the publisher default checkpoint and buzz budget on a block install. Until a publisher-facing install UI ships (separate UX initiative), this is how the demo install (mbi_01KSD3NP23EQHXEPQRH32EX72G) gets configured. Routes through BlockRegistry.updateSettings so the same per-block-id validation runs (ecosystem match, Published status, Checkpoint type). Actions: set-default-checkpoint, set-buzz-budget, show. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(blocks): checkpoint service + router precedence chain 19 new tests covering the checkpoint resolution chain end to end. checkpoint.service.test.ts (15 tests): - validateBlockCheckpoint: every failure mode with distinguishable cause.reason (not-found / not-published / not-a-checkpoint / wrong-ecosystem); same-family-different-baseModel match (Flux.1 D ↔ Flux.1 S). - getRepresentativeBaseModel: published → unpublished fallback → null. - resolveBlockCheckpoint: Checkpoint-self short-circuit (no DB reads), viewer override beats publisher default, stale viewer override drops through to publisher default, no override + no default = BAD_REQUEST, publisher-default validation failures surface (not silenced). blocks.router.workflow.test.ts (4 new tests in LoRA-install describe): - BAD_REQUEST when no publisher default AND no override - publisher default used when override missing - viewer override beats publisher default - stale override falls through cleanly The original 16 router tests still pass — they use Checkpoint-type fixtures that short-circuit through resolveBlockCheckpoint's self-anchor branch, so the new precedence chain doesn't regress them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): platform per-ecosystem checkpoint fallback + fix picker filter Two changes that make LoRA installs Just Work without per-install configuration. 1. Picker filter normalization The block sends effectiveCheckpoint.baseModel (e.g. "Flux.1 D") to OPEN_CHECKPOINT_PICKER, but getBaseModelsByGroup expects an ecosystem key (e.g. "Flux1"). Result: empty filter → no checkpoints visible in the picker. Wrap with getBaseModelGroup, which accepts both forms and normalizes to the ecosystem key. 2. Platform per-ecosystem fallback New rung in the precedence chain: when no publisher default AND no viewer override, pick the most-thumbed Published Checkpoint with at least one version in the LoRA's ecosystem family. Cached in Redis 1h. Used by both resolveBlockCheckpoint (submit-time) and BlockRegistry.getEffectiveCheckpoint (BLOCK_INIT-time) so the iframe and the orchestrator agree on the same anchor. BAD_REQUEST is now only thrown when the ecosystem has zero Published Checkpoints — a real edge case (brand-new base model with only LoRAs). The "ask the model owner" message is gone for normal installs; the demo works out of the box on any ecosystem with a popular Checkpoint. Adds REDIS_KEYS.BLOCKS.POPULAR_CHECKPOINT for the 1h cache; outage fails open to the DB query. 22 new test cases (4 new in router; 5 new + 1 updated in service). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): pivot popular-checkpoint query through ModelMetric CI Type Check failed: Prisma can't `orderBy` a scalar through a 1:many relation (Model.metrics is declared as ModelMetric[] even though @@id([modelId]) makes it 1:1 in practice). The model.findFirst with orderBy: { metrics: { thumbsUpCount: 'desc' } } typechecks locally on the stale Prisma client but fails on a fresh one — CI hit the real generated types. Start the query from ModelMetric instead: orderBy the scalar directly, filter the related model by Checkpoint + ecosystem + Published, project the model + its top version through the metric. Same logical query, two-rows-deep instead of one. Updated test fixtures to wrap the model in a metric envelope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): showcase images in BLOCK_INIT.context for carousel UX New tRPC blocks.getShowcaseImages(modelVersionId): up to 6 published images for the version, de-duped, ordered by all-time reactionCount, with the standard gen-meta fields (prompt, negativePrompt, cfgScale, steps, seed, sampler) defensively extracted from the wide Image.meta JSONB. Public (showcase images are already public on the model page). IframeHost calls the query in parallel with getEffectiveCheckpoint and merges into BLOCK_INIT.context.showcaseImages so the block can render a carousel + populate gen params from the user's pick without an extra round-trip on mount. ModelSlotContext + ShowcaseImage type extended on the host side; the SDK mirror lands in a follow-up commit. 8 new tests covering reaction-sort, de-dupe, missing-metric fallback, and meta extraction across camelCase / A1111 PascalCase / malformed shapes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): block_user_subscriptions table + types Adds the schema substrate for user-controlled block installs: two scopes ('publisher_all_my_models', 'viewer_personal'), one table, three partial indexes (two for the listForModel hot paths, one for the management UI), and the wire shapes the new tRPC procedures will consume. * feat(blocks): BlockRegistry methods for user subscriptions + marketplace Adds four service-layer entry points consumed by the new tRPC procedures and management UI: - listUserSubscriptions: rows for the current user, both scopes, with the app block denormalised for rendering - upsertSubscription: idempotent write against the composite unique (userId, appBlockId, scope). Empty target arrays land as Postgres TEXT[] so the SQL array_length predicate normalises them back to 'no filter' at read time - deleteSubscription: owner-checked, idempotent on missing rows - listAvailable: marketplace listing with slot/query/cursor paging and install_count desc sort Tests cover idempotency, the owner gate on delete, target-array normalisation, and the listAvailable SQL shape. * feat(blocks): listForModel honours user subscriptions with viewer ctx Extends listForModel SQL with two new UNION branches: - source_rank 2: publisher_all_my_models subscriptions where Model.userId joins bus.user_id (transferring a model swaps which user's subs apply automatically) - source_rank 4: viewer_personal subscriptions where the current viewer's userId matches; anon viewers (-1 sentinel) match no rows Platform defaults move from rank 2 to rank 3 to slot between them. Each subscription branch carries target_model_types and target_base_models filters; empty arrays normalise to 'no filter' via array_length(...) IS NULL. The viewer branch carries three NOT EXISTS clauses so a higher-rank source already showing the same app_block + slot suppresses the duplicate. Caching: per-viewer correctness ranks higher than cache-hit rate in v1, so listForModel skips Redis entirely when viewerUserId is set. blocks.listForModel tRPC procedure now passes ctx.user?.id. Tests cover the four source ranks, the bus.user_id join, the -1 sentinel for anon, the cache disable on viewerUserId, and that two viewers don't see each other's cached results. * feat(blocks): tRPC procedures for subscriptions + marketplace Adds four procedures on blocksRouter: - listMySubscriptions (guarded) — both scopes for the current user, fail-soft to [] when the appBlocks flag is off - listAvailable (public) — marketplace listing with slot/query filter + cursor paging, fail-soft to empty when flag off - upsertSubscription (guarded) — validates settings through blockSettingsSchemaByBlockId and the 4KB cap, requires status='approved' on the target app block - deleteSubscription (guarded) — service-layer owner check, idempotent on missing rows Tests cover anon rejection, flag-off behaviour, app-block status gates, per-block-id settings validation (out-of-range buzz budget), and forwarded-argument correctness for both happy and error paths. * feat(blocks): /apps marketplace + per-app settings modal Adds three UI surfaces: - /apps marketplace page with slot-filter chips, debounced search, grid of AppBlockCard rendering name/description/slot/install count, gated on useFeatureFlags().appBlocks - AppBlockCard component used by the marketplace and (later) the installed page - AppSettingsModal: the per-app settings panel from the spec. Two scope toggles (publisher_all_my_models, viewer_personal), multi-select chips for target model types and base models, NumberInput for buzz_budget_per_gen, openResourceSelectModal integration for the default-checkpoint picker (reused from the checkpoint-selector handoff). Each scope toggle independently calls upsert / delete so the user can persist one scope without committing the other. SSR gate uses features.appBlocks + the standard session redirect. * feat(blocks): /apps/installed management page Lists the current user's subscriptions split into two sections — 'On models I own' (publisher_all_my_models) and 'On model pages I view' (viewer_personal). Each row carries: - block name + scope badge + filter chips (model types, base models) - inline enable/disable toggle (upsertSubscription with enabled flip) - settings gear → opens the same AppSettingsModal as the marketplace - trash → deleteSubscription with optimistic invalidate Empty states link back to /apps. SSR gates on features.appBlocks and the standard session redirect. * feat(blocks): publisher-subscription banner on model detail Owner-only Alert shown on the model detail page when one or more publisher_all_my_models subscriptions target this model (filtered client-side by model type; base-model filter applies server-side in listForModel). Each row offers: - 'Edit subscription' → /apps/installed - 'Disable for this model only' → installOnModel + toggleEnabled false (writes a per-model row that suppresses the subscription via NOT EXISTS in listForModel) The banner is hidden for non-owners and when the appBlocks feature flag is off. * feat(blocks): user-menu links to Apps marketplace + installed page Two new menu items in the user-state group, both gated on features.appBlocks (the same flag the slot rendering uses): - 'Apps' → /apps (newUntil: 2026-07-01) - 'Installed Apps' → /apps/installed The flag is currently availability: ['mod'] in feature-flags.service.ts, so non-mods won't see the items at all. Expanding to ['mod', 'member'] is the next rollout step per the handoff. * fix(blocks): typecheck cleanups for subscription paths - Meta on /apps and /apps/installed now sets deIndex (Meta's discriminated union requires either deIndex or canonical) - block-registry.service.ts: cast $queryRaw result to Row[] and annotate the .map callback (matches the listForModel pattern) - listUserSubscriptions: define an explicit SubRow type and cast the findMany result so the local typecheck stays green while the Prisma client is stale (CI regenerates the client on every build) * chore(blocks): silence editor diagnostics on subscription typings - subscription.schema.ts: replace deprecated zod .merge() with shape spread - subscription test mocks: type vi.fn() args/returns so mockResolvedValue payloads typecheck cleanly and mock.calls[N] index access works * feat(blocks): BlockRegistry.resolveBlockInstance for synthetic ids Adds a centralised lookup that translates a blockInstanceId of any kind — real install (bki_*), platform default (pdb_*), publisher subscription (bus_pub_*), viewer subscription (bus_view_*) — into the install-shape struct downstream code (token mint, settings update, workflow submit) consumes. Returns null when the instance doesn't resolve OR when the caller-supplied (modelId, slotId, viewerUserId) don't match what the source row would actually surface on listForModel. This is the cross-row gate that keeps an authenticated iframe from minting a token for a model the resolved source doesn't surface — for synthetic ids the row is per-user, not per-model, so the caller-supplied context is the only auth pin. Predicates mirror listForModel SQL (block-registry.service.ts:280-484): - mbi/bki_*: row.modelId == modelId, row.slotId == slotId, enabled, approved - pdb_*: enabled, slot matches, target_model_types filter, no install suppressor - bus_pub_*: scope, enabled, approved, manifest targets slot, Model.userId == bus.user_id, target_model_types + target_base_models filters, no install suppressor - bus_view_*: viewer == bus.user_id (anon never resolves), scope, enabled, approved, manifest targets slot, filters, AND cascading rank 1/2/3 suppressors (per-model install, publisher sub, platform default) 25 unit tests pin the cross-row re-validation for each source path, including malformed ids and rank-by-rank suppression for viewer subs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): block-tokens endpoint resolves synthetic instance ids Pre-fix, POST /api/v1/block-tokens did a raw modelBlockInstall.findUnique({where:{blockInstanceId}}) and 404'd for every blockInstanceId namespace except real installs (bki_*). Platform defaults (pdb_*), publisher subscriptions (bus_pub_*), and viewer subscriptions (bus_view_*) — all valid sources surfaced by listForModel — returned "Block install not found", blocking the iframe from minting a token. Replaces the lookup with BlockRegistry.resolveBlockInstance, which handles all four namespaces and re-validates the caller's (modelId, slotId) against the source row before mint. The validated modelId/slotId from the resolved row are what reach the JWT ctx — caller-supplied values in slotContext are never trusted for binding claims. slotContext is now schema-required to include modelId/slotId (the iframe host already sends both via useBlockToken.ts:96). Extra fields still flow through to BLOCK_INIT.context for display but are dropped from the JWT. The settings-scope publisher check at lines 411-418 keeps comparing against install.installedByUserId — for subscription sources this is set to bus.user_id (the subscription owner is the "publisher" for their own settings), which is the right semantic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): workflow + checkpoint paths resolve synthetic instance ids Wires BlockRegistry.resolveBlockInstance into the remaining two reads that fail for synthetic blockInstanceIds: 1. BlockRegistry.getEffectiveCheckpoint — called by the IframeHost pre-BLOCK_INIT to fill context.checkpoint. Now accepts modelId + slotId as the resolver's auth pin and reads publisher settings from the resolved source row (install/subscription/platform default). The tRPC procedure widens its input accordingly; the IframeHost forwards modelCtx.modelId and modelCtx.slotId. 2. resolveBlockCheckpoint (checkpoint.service.ts) — called from submitWorkflow/estimateWorkflow with claims.blockInstanceId. Now reads publisher settings via the resolver so a JWT minted for a bus_pub_* / bus_view_* / pdb_* synthetic id correctly resolves its publisher's default_checkpoint_version_id. The routers forward claims.ctx.slotId (stamped by block-tokens) to satisfy the resolver's auth pin. Adds one workflow router test that exercises submitWorkflow with a bus_pub_* JWT end-to-end and verifies the resolver was called with the correct (blockInstanceId, modelId, slotId) tuple from JWT ctx. updateSettings, toggleEnabled, and uninstallFromModel keep their direct modelBlockInstall.findUnique paths intentionally: those endpoints operate exclusively on real installs (bki_*) — subscription settings have their own write path via blocks.upsertSubscription, and platform defaults aren't settings-writable at all. A synthetic id reaching those endpoints is a client bug; the 404 they return today is the correct fail-closed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(blocks): type vi.fn() args on block-tokens test mocks * fix(blocks): showcase reads ImageResourceNew (legacy table is empty) * feat(blocks): inherit clipSkip from showcase image meta Mirrors what the platform's Remix flow extracts from Image.meta (getMediaGenerationData reads meta.clipSkip ?? meta['Clip skip']). Forwards through the block-side schema, workflow input builder, ShowcaseImage type, and the SDK-mirror in components/AppBlocks. * fix(blocks): prefer meta-recorded gen dims over image file dims in showcase Many showcase images are generated at one resolution (e.g. 832x1216) and upscaled offline to a higher resolution (e.g. 2496x3648) before being uploaded. Image.width/Image.height reflect the post-upscale file; meta.width/meta.height reflect the actual generator output. The block was reading file dims, so the user picking a showcase image got a generation at ~3x area — even with seed/cfg/steps/sampler/ clipSkip identical, the composition diverged noticeably from the showcase (real-world: workflow 8753561-20260525223849768 ran at 1408x2048 from a 2496x3648 image whose meta said 832x1216). Falls back to file dims when meta lacks width/height (older images, non-SD pipelines). Block-side clamp still scales anything over 2048. * feat(blocks): block_buzz_attribution schema + BlockAttribution type One row per buzz purchase originated inside an App Block. Drives the publisher revenue-share payout pipeline. block_instance_id is TEXT (not FK) because it can resolve to mbi_/bus_pub_/bus_view_/pdb_ — the scope column tells the reader which surface owns the id. app_owner_user_id and rate_card_version are snapshot at attribution time so past revenue stays stable when ownership or rate cards change. Includes a share-sum CHECK constraint (provider_fee + platform_share + app_owner_share = usd_amount) so arithmetic bugs in the rate-card calculator surface at write time. Adds the BlockAttribution schema with deriveScopeFromInstanceId + encode/extract helpers used by the modal, the iframe host, and all three payment-provider webhook paths. * feat(blocks): rate card v1 with placeholder publisher share pcts Defines RATE_CARD_V1 (active) with the four scope-based publisher cuts agreed during planning: 20% / 20% / 25% / 0%. computeRateCardSplit takes gross + provider fee + scope and returns the three-way split that satisfies the migration's share-sum CHECK constraint. Important: percentages are PLACEHOLDER pending monetization-leadership sign-off — soft-launch only. The handoff doc enumerates the open items. Rate cards are never mutated in place — new versions = new exported constants, past attributions pay out under their snapshot. 10 unit tests cover clean splits per scope, self-purchase / internal- owner zero overrides, fractional-cent flooring (publisher never overcollects), negative-gross clamping, and the active-card invariant for every scope. * feat(blocks): BlockBuzzAttribution.record service + void path Writes one block_buzz_attribution row per (paymentTransactionId, appBlockId) — idempotent via the unique constraint. Resolves the app owner from OauthClient at write time and snapshots userId onto the row so payouts are stable when ownership later changes. Self-purchase wash (purchaser == publisher) writes the row with status='voided', voided_reason='self_purchase', publisher share = 0 — audit-friendly without erroring the buzz credit. Internal app owners get the same zero-share treatment via the rate card. P2002 idempotency uses duck-typing on err.code instead of instanceof Prisma.PrismaClientKnownRequestError so the path works even when the Prisma client is stale at runtime (CI worktrees). voidAttributionsForPayment flips rows to voided on refund/chargeback — used by the upcoming refund webhook integration. REFUND_WINDOWS_DAYS holds the per-provider refund windows for the confirm-pending cron job (next phase). 12 unit tests cover: per-scope share math, self-purchase voiding, idempotent retry, missing-app guard, modelId / buzzTxId flow-through, audit log emission, void+refund path. * feat(blocks): Stripe webhook records attributions + voids on refund payment_intent.succeeded now writes a block_buzz_attribution row after the buzz credit lands, when the payment-intent metadata carries block* keys. Skipped silently on test-mode events (livemode=false), skipped silently when no attribution keys are present (the steady- state for every non-block buzz purchase) — no regression risk on unrelated buzz flows. Provider fee is pulled from the charge's balance_transaction so the publisher cut comes off the actual Stripe net, not gross. Falls back to 0 if the expansion fails — share-sum CHECK still holds because the calculator constructs (fee, platform, publisher) from a single gross. charge.refunded / charge.dispute.created now flip matching attribution rows to voided alongside the existing referral-kickback revoke. If the row was already paid out, status='voided' is still set and the payout reconciliation job claws back from the next payout. Attribution write failures are logged but never fail the webhook — the buzz credit has already happened, and Stripe's retry policy plus the UNIQUE constraint make the write idempotent on retry. 12 schema tests cover the metadata roundtrip, prefix→scope resolver, and corrupt-input rejection. * feat(blocks): Paddle webhook records attributions on buzz purchase processCompleteBuzzTransaction now writes a block_buzz_attribution row after the buzz credit lands, when the price-level customData carries block* keys. Same skip-on-no-attribution + idempotent-on-retry shape as the Stripe path. Provider fee left at 0 for v1 (TODO — paddle's SDK doesn't surface fees on the line item; needs a transactions.get). Refund/chargeback void path is NOT wired for Paddle because the existing webhook handler doesn't subscribe to TransactionAdjusted / adjustment.created events. Paddle is in maintenance mode per the header comment in webhooks/paddle.ts — no new signups flow through it. Reconciliation will run against the Paddle adjustments API periodically; the void call should land in that path when it ships. Captured in the handoff doc as a known gap. * docs(blocks): document why NOWPayments has no buzz attribution NOWPayments uses shared per-user deposit addresses + order_id = 'user:{userId}' with no per-purchase metadata bag, so attribution can't ride through to the IPN webhook. Two ways to wire it up are called out (session table or per-purchase addresses) but both are out of scope for v1. In-block crypto buzz purchases will credit buzz normally but write no attribution row — publishers don't earn share on those. Captured here so the next implementer sees the constraint before chasing it. * feat(blocks): IframeHost passes attribution into BuyBuzzModal OPEN_BUZZ_PURCHASE now derives an attribution payload from the install context (appId, appBlockId, blockInstanceId + prefix-resolved scope + optional modelId) and threads it through: IframeHost → BuyBuzzModal → BuzzPurchaseLayout → BuzzPurchaseImproved → Stripe paymentIntent metadata. The iframe never supplies these fields itself — host-derived only — so a malicious block can't forge attribution to a different app or publisher. Unknown blockInstanceId prefix → undefined attribution → no row written (defensive fail-closed). To enable this, BlockInstall / BlockInstallRecord gained an appBlockId field (the app_blocks.id, distinct from the manifest block_id) and listForModel's SQL selects it from all four union arms. The deriveScopeFromInstanceId resolver now handles both mbi_ and bki_ prefixes (legacy unique-column on the same install row) in lockstep with BlockRegistry.resolveBlockInstance. 13 schema tests + the rest of the buzz-attribution test suite stay green. The 3 pre-existing checkpoint-service failures are unrelated (stale Prisma in the worktree). Paddle path is left without attribution threading in the modal — the current Paddle flow in BuzzPurchase.tsx has its Stripe handler commented out and paddle is in maintenance per the webhooks/paddle.ts header. Stripe is the only live attribution path for v1. * feat(blocks): daily cron promotes pending attributions to confirmed Runs at 03:15 UTC daily, one updateMany per provider. Stripe gets a 30-day window, Paddle 14d, NOWPayments 1d (window constants live in buzz-attribution.service.ts and are imported here so the cron and the rate-card stay in lockstep). Idempotent on the WHERE clause — only filters status='pending'. Already-confirmed/voided/paid_out rows are inert. 3 unit tests assert the per-provider cutoff math + status invariant. The refund void path was wired in phase 4 (Stripe webhook); this job is what makes the confirmed → paid_out pipeline work on the happy path. * feat(blocks): bulk-payout stub job + handoff for payout pipeline Runs Mondays 09:30 UTC. Currently writes NOTHING — just aggregates status='confirmed' rows by app_owner_user_id and logs the queue depth + dollar total to Axiom for observability. Automation of the actual payout is blocked on monetization-leadership decisions, all enumerated in the job's header doc: - Money flow: integrate with creator-program cash bank (UserPaymentConfiguration + Tipalti, couples to compensation pool cap logic) OR mint a separate Tipalti payment via payToTipaltiAccount (skips pool guards + 1099 routing). - UserPaymentConfiguration prerequisite + missing 'earnings ready' notification for App Blocks publishers. - Refund clawback on paid_out rows (Tipalti adjustment shape). - 1099 / tax reporting — only flows through the cash bank path. Until those land, publishers see 'confirmed' rows accumulate on the dashboard but no auto-disbursement fires. Leadership can batch-process the queue manually using the Axiom log + the per-publisher breakdown in the log payload. * feat(blocks): blocks.getMyRevenue + blocks.getMyApps tRPC procedures Two guardedProcedure queries gated on the App Blocks feature flag: blocks.getMyRevenue({ appBlockId?, from?, to? }) Aggregate revenue summary for the caller across pending/confirmed/ paid_out/voided buckets, plus the top 5 earning apps and the 50 most recent attribution rows. Service filters by appOwnerUserId so a request with someone else's appBlockId returns empty. blocks.getMyApps() Owned apps + lifetime confirmed+paid_out revenue per app. One groupBy across all apps so the request stays sub-linear. Service-side helpers (getRevenueForOwner, getRecentAttributionsForOwner) fire 4-5 small aggregate queries in parallel — the bba_publisher_dashboard_idx on (app_owner_user_id, attributed_at DESC) keeps each one cheap. Pages render off these two endpoints in the next phase. * feat(blocks): /apps/revenue + /apps/[appBlockId]/revenue pages Two new pages reading from blocks.getMyRevenue / blocks.getMyApps: /apps/revenue 4 summary cards (pending / confirmed / paid out / voided), Top 5 earning apps (links into the per-app page), recent attributions table. /apps/[appBlockId]/revenue Same summary cards filtered to one app, recent attributions table scoped to that app. Owner check on the client surface: myAppsQuery.data.find((a) => a.id === appBlockId); if not in the owner's list we fail closed with NotFound. Server-side service filter (appOwnerUserId in the WHERE) is the actual auth gate — the UI NotFound just keeps the intent legible. Local type aliases on the page level because RouterOutputs isn't exported in this codebase + the worktree's stale Prisma client reduces inferred query data to {}. CI will type-narrow correctly when the client regenerates. Out of scope for v1 (deferred per the spec): timeseries chart, date range picker, CSV export, scope breakdown stacked bar. * feat(blocks): App Revenue nav link + earning chip on marketplace cards User menu (gated on features.appBlocks) gets a new 'App Revenue' entry below 'Installed Apps', linking to /apps/revenue. Green IconCurrencyDollar to visually distinguish the earnings flow from the install management flow. Marketplace cards (/apps) gain an 'Earning $X.XX' badge when the viewer owns the app and has lifetime confirmed+paid_out share > 0. Chip is suppressed when ownedEarningCents is undefined (not owned) or 0 (owned but no earnings yet) — the upsell is 'you're earning', not 'you could earn'. ownedEarningCents flows from blocks.getMyApps which is guarded by guardedProcedure, so non-owners never see anyone else's earnings. * feat(blocks): prometheus counter for buzz attribution writes Adds civitai_app_block_buzz_attribution_total with provider/scope/ status labels. Incremented in BlockBuzzAttribution.record after the DB write succeeds — best-effort try/catch so metric infrastructure issues never back-pressure the webhook. Audit logs (logToAxiom 'block-buzz-attribution' channel) already landed in phase 3 + phase 4. With the counter in place, ops gets: - per-provider funnel (Stripe vs Paddle attribution-write volume + scope mix) - self-purchase wash visibility (status='voided' rows show up immediately on the metric, so a spike implies someone gaming their own app) - confirmed/voided lifecycle dashboards are still possible via follow-up counters (next change — pending → confirmed promotion + refund void path could each get their own). The promised pending_share_cents gauge from the original handoff is not wired in this commit — it requires a periodic exporter against the dbRead aggregate query, and the bulk-payout stub job already logs that same number for ops via Axiom. Add the gauge once the payout pipeline lights up so dashboards can show 'money waiting to pay out' alongside 'money paid out'. * fix(blocks): add missing OauthClient.buzzAttributions back-relation Prisma schema validation rejected BlockBuzzAttribution because the `app` relation lacked an opposite-side back-reference on OauthClient. AppBlock already had buzzAttributions; OauthClient was missed when the subagent landed the model. Caught by Tekton typecheck step (P1012). * ci: re-trigger preview build * chore: trigger preview re-run after infra-side failure * fix(blocks): break Prisma groupBy type back-propagation in bulk-payout Prisma's groupBy uses constrained generic inference: a direct `as GroupRow[]` cast on the await result back-propagates into the args type as an intersection (`& GroupRow[]`), which then fails to validate the args object. Cast through `unknown` to break the back-propagation. Caught by Tekton typecheck (TS2345). Local typecheck didn't surface it because db:generate is broken in the worktree. * feat(blocks): rate card v2 + payout routing/clawback recommendations Rate card v2 lands the recommended starting percentages: per_model_install: 20% -> 15% (most counterfactual) publisher_all_my_models: 20% -> 15% (same) viewer_personal: 25% (kept; most incremental) platform_default: 0% (kept; mod-promoted) V1 stays defined for history. ACTIVE_RATE_CARD now points at V2. Start lower; raising via V3 later is politically easier than lowering after a public announcement. bulk-payout-block-attributions.ts header documents the recommended implementation path: route money through creator-program cash bank (1099 + existing UserPaymentConfiguration), carry-forward debt for refund clawback (affiliate-network standard, transparent ledger). Job remains a stub pending monetization leadership sign-off. * feat(blocks): autoclaim daily boost reward when user balance is short Submit flow now opportunistically claims the daily boost (25 blue Buzz, one per UTC day) when a user clicks Generate on a block and their actual Buzz balance would be short — but only when the claim would close the gap. If the boost wouldn't be enough on its own, the claim is skipped so the one-per-day reward isn't burned on a still-hopeless submit. Gate is conservative: precheck balance+details, only call apply() when (1) boost is unclaimed today, (2) current spendable balance < cost, and (3) balance + awardAmount >= cost. apply() is idempotent (Redis Lua dedups per UTC day) and any failure is logged + swallowed so submit still proceeds and surfaces the existing Top-Up CTA. Snapshot grows an optional `autoClaim` field that the iframe can use to surface a "+25 daily boost claimed" notice; the SDK type mirror is bumped to 0.5.0 in a sibling repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(app-blocks): W3 v0 — manifest-driven settings + generic form renderer (#2334) * feat(blocks): W3 v0 phase 1 — manifest-driven settings meta-schema + generic validator Adds the meta-schema that the W2 webhook handler will validate manifests against on push, plus the runtime validator that replaces the per-block-id schema map at every settings call site. manifest-settings.meta.schema.ts — record<snake_case_key, SettingField>: - discriminated union over type=number|string|boolean - scope=publisher|viewer + requires_scope gating - widget hints (number/slider/resource_picker, text/textarea/select, toggle) - cross-field checks: min<=max, default in range, select needs enum, RegExp parses settings-validator.service.ts — validateBlockSettings({manifest, input, scopes, forScope}): - wrong-scope fields silently skipped (single fn validates either side) - requires_scope filter for app-scoped feature gating - defaults applied, unknown keys stripped without leaking which were unrecognized - explicit null preserved when field declares default:null (resource picker case) - TRPCError(BAD_REQUEST) per offending field so install-form UI can surface inline 50 unit tests across both files exercise the happy paths, scope filtering, null handling, and every per-type failure mode. Phase 2 (call-site migration + deleting blockSettingsSchemaByBlockId) lands separately so this commit can stand on its own — W2 can import manifestSettingsSchema for its webhook validator before phase 2 ships. * feat(blocks): W3 v0 phase 2 — migrate call sites to generic manifest validator Replaces the per-block-id settings schema map with manifest-driven shape validation at every settings write. Deletes settings.schema.ts (the in-tree typed schemas + blockSettingsSchemaByBlockId lookup). Settings call sites migrated: - block-registry.installOnModel: fetch manifest + approvedScopes, run validateBlockSettings(forScope=publisher) + checkpoint cross-row check. - block-registry.updateSettings: same. - block-registry.upsertUserSettings / getUserSettings: param + return now Record<string, unknown> instead of BlockUserSettings. - block-registry.getEffectiveCheckpoint: read raw publisher / viewer values with typeof guards (validation already enforced at write time). - blocks.upsertSubscription router: fetch manifest, derive forScope from subscriptionScope, run generic validator. - blocks.updateUserSettings router: accept generic settings record, resolve install via resolveBlockInstance for manifest+scopes, validate with forScope=viewer, keep the checkpoint cross-row check. - checkpoint.service.resolveBlockCheckpoint: read raw checkpoint_version_id + default_checkpoint_version_id with typeof guards. The static manifest is the contract; cross-row checks (checkpoint must exist + share ecosystem) stay as adjacent special-cases since they need DB reads the meta-schema can't express. Third-party apps in v1 will be able to add settings without a civitai-side PR — this is the v0 substrate for that. Phase 1's 50 manifest-settings + settings-validator tests still pass. Block-tokens, showcase, workflow, attribution.schema, rate-card unit tests unaffected (100 tests green across the touched modules). * deps: add @civitai/app-sdk + @civitai/blocks-react W3 + W4 work imports from @civitai/app-sdk/blocks (ManifestSettings, SettingField, app-storage message types) and @civitai/blocks-react/ui (SettingsForm) + @civitai/blocks-react hooks (useAppStorage). Both packages publish from civitai/civitai-app-starters PR #11. DO NOT MERGE this commit until the npm publish lands: npm view @civitai/app-sdk@0.6.0 version # → 0.6.0 npm view @civitai/blocks-react@0.4.0 version # → 0.4.0 After publish + merge, `pnpm install` regenerates pnpm-lock.yaml. W4 (zach/w4-kv-datastore) and W2 (zach/w2-phases-2-7) inherit these deps when they rebase onto this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(app-blocks): W2 v0 phases 2-7 — civitai-web service layer + Submit UI + schema (#2336) * feat(blocks): W2-v0 Phase 3 — Forgejo client + apps-pipeline + webhooks Three pieces of plumbing that connect Forgejo pushes to the per-app deploy on dp-1: - src/server/services/blocks/forgejo.service.ts REST wrapper for Forgejo: createRepoFromTemplate, addCollaborator, ensurePushWebhook, getRawFile, setCommitStatus. Talks to forgejo-http.forgejo.svc.cluster.local in-cluster or https://forgejo.civitaic.com from PR-preview envs. - src/server/services/blocks/apps-pipeline.service.ts Two cross-cluster k8s API helpers. triggerBuild() POSTs a PipelineRun to dc-02-a's Tekton via a mounted kubeconfig (APPS_TEKTON_KUBECONFIG). triggerApply() POSTs an apply Job to dp-1's civitai-apps namespace via the in-pod default-SA token (RoleBinding lives in datapacket-talos clusters/production/apps/civitai-apps/rbac.yaml). - src/pages/api/internal/blocks/git-push.ts Forgejo push webhook. HMAC-verifies, requires app-blocks-enabled Flipt flag, looks up app_blocks row by slug, fetches the manifest at the new SHA, validates against BlockManifestValidator + canonical iframe.src host pattern, upserts, triggers build, writes pending commit status. - src/pages/api/internal/blocks/build-callback.ts Tekton finally-task callback. HMAC-verifies the shared secret, flips commit status, triggers the apply Job, updates current_version_deployed_at. The DB column lives in the Phase 4 migration (next commit). Env additions (all optional so envs without the platform layer still boot): FORGEJO_BASE_URL, FORGEJO_ADMIN_TOKEN, FORGEJO_WEBHOOK_SECRET, BLOCK_BUILD_CALLBACK_SECRET, APPS_TEKTON_KUBECONFIG, APPS_TEKTON_NAMESPACE (default tekton-builds), APPS_KUBE_NAMESPACE (default civitai-apps), APPS_DOMAIN (default apps.civitaic.com). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W2-v0 Phase 4 — blocks.submitApp + Submit UI + schema - prisma/schema.full.prisma + new migration 20260526200000_app_blocks_repo_versioning adds current_version_sha, current_version_deployed_at, repo_url to app_blocks. All nullable — hackathon rows pre-W2 stay valid until W12 cutover. Per CLAUDE.md gotcha #14, the migration must be applied manually via psql. - src/server/routers/blocks.router.ts — adds the submitApp mutation (civitai-team gated). Creates a Forgejo repo from civitai-apps/starter via the new forgejo.service, attaches a push webhook pointing at /api/internal/blocks/git-push, and inserts a pending app_blocks row with apb_<ULID> id (mirrors existing hackathon convention; the v1 developer-endpoint at /api/v1/developer/block-manifests uses ab_). - src/server/utils/app-block-ids.ts — newUlid() now public so callers needing a non-standard prefix can compose their own. - src/pages/apps/submit.tsx — Civitai-team-only form. Slug + OauthClient picker + description. Success state shows repo URL, clone command, public URL, and the new appBlockId. - src/pages/apps/index.tsx — Submit App button on the marketplace header, mod-gated client-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(app-blocks): W4 v0 — isolated KV datastore (cnpg-cluster-apps) (#2335) * feat(apps): AppStorageProvisioner + appsDb client (W4-KV-v0 P2) Substrate for the App Blocks KV datastore. Adds the connection to the new cnpg-cluster-apps cluster (datapacket-talos commit eb86f3a33) and the idempotent per-app schema/role provisioner. tRPC procedures + SDK + IframeHost handlers land in later phases. - `APPS_DATABASE_URL` server-schema env (optional — appsDb is null in environments that don't have the apps cluster wired). - `getClient({ instance: 'apps' })` extension; new appsDb singleton in `src/server/db/appsDb.ts` mirrors the notifDb pattern. - `sanitizeAppSlug` / `isValidAppSlug` / `appSchemaIdent` / `appRoleIdent` in `src/server/utils/apps-slug.ts`. Regex `^[a-z][a-z0-9_]{2,40}$` is the load-bearing safety boundary — identifiers can't be parameterized in pg so DDL leans entirely on this gate. - `AppStorageProvisioner.{provision,deprovision,getQuota}` in `src/server/services/apps/storage-provision.service.ts`: * Schema, kv table (with generated size_bytes column), quota table, trigger function, trigger, role + grants, default privileges, seed row — all inside a single client.query('BEGIN') / 'COMMIT'. * Trigger pulls app_block_id from session-local `app.current_app_block_id` so the tRPC procedure layer scopes writes via SET LOCAL inside the same txn. Missing GUC no-ops to avoid hard-failing the user path. * Idempotent — IF NOT EXISTS on every DDL + DO-block guards on role creation; ON CONFLICT on the quota seed. * Slug + appBlockId validated before pool.connect() so bad input can't even reach the wire. - Unit suites for the slug helper (28 assertions) and the provisioner (mocked pg client; checks txn boundaries, identifier quoting, the parameterized quota seed, rollback on mid-DDL failure, and the getQuota happy/empty/missing-schema paths). Out of scope for this phase: tRPC procs (P3), SDK hook (P4), IframeHost handlers (P5), metrics + audit (P6), backfill + hackathon provision (P7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(apps): apps.storage.* tRPC procedures + tests (W4-KV-v0 P3) Five host-mediated procedures behind the block JWT: - `apps.storage.get(key)` — null for anon viewers, scoped to (block_instance, user). - `apps.storage.set(key, value)` — 64KB per-value cap, 50MB per-app quota gate (uses the per-row size delta on update so a shrink doesn't falsely trip), single-connection SET LOCAL → INSERT … ON CONFLICT. - `apps.storage.delete(key)` — same connection-scoped txn shape. - `apps.storage.list({ prefix, limit, cursor })` — keys-only, cursor pagination, LIKE wildcards in user-supplied prefixes are escaped. - `apps.storage.getQuota()` — surfaces used/row counts + the v0 caps so client UIs don't hard-code the ceiling. `resolveStorageContext` is the shared gate — verify the JWT, validate the slug via the regex helper, look up the AppBlock by (appId, blockId) to enforce status='approved' and pull the appBlockId for quota keying, parse userId from the sub. Every gate emits an `app_blocks_storage_ops_total` counter increment with op + outcome so dashboards can pin failure mode without log-side correlation. Mounted as `apps` (new top-level tRPC router) — sibling to `blocks`. v1 will extend `apps.*` with `sql.query` + `migrate.run`; the namespace is intentional even though there's only one sub-router today. Tests (vi-mocked pool + verifier + dbRead + provisioner): - Flag dark, bad token, missing/unapproved AppBlock, malformed slug. - Anon-viewer null returns on get/list; UNAUTHORIZED on set/delete. - get returns DB value + uses the correct schema-quoted SQL. - set: per-value cap, quota gate including the shrink-allowance via net delta, happy-path transaction shape (BEGIN → SET LOCAL → INSERT → COMMIT) plus client release. - delete reports rowCount > 0 vs 0. - list paginates only when the page filled; LIKE-special chars escaped. - getQuota proxies the provisioner snapshot + surfaces v0 limits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): IframeHost storage handlers (W4-KV-v0 P5) Wires the five `APP_STORAGE_*` bridge messages into the new `apps.storage.*` tRPC procedures. Same pattern as the existing workflow bridge — each handler validates the incoming postMessage shape, calls into the procedure with the block token, and posts a result back to the iframe with the same requestId. - get / list / getQuota — imperative fetch via `trpc.useUtils()` (procedures are tRPC `.query()` but the call site is a one-shot message handler, not a reactive subscription). - set / delete — mutations. - Errors are surfaced via `error: <string>` on the result payload so the SDK hook can reject; handlers NEVER throw upward and strand the postMessage round-trip. - `storageErrorMessage` keeps surfacing conservative — uses the TRPCClientError `.message` when available, falls back to a generic string. The iframe is untrusted so we don't leak stack traces. - List handler clamps user-supplied `limit` to [1, 200] and rehydrates `updatedAt` from Date → ISO on the wire (the SDK rehydrates on the block side). Paired with the apps.storage router (P3) + the useAppStorage SDK hook (P4, civitai-app-starters branch zach/w4-storage-sdk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(apps): storage latency histogram + per-write audit (W4-KV-v0 P6) Closes the metrics + audit-log bullet on the W4-v0 acceptance list. - `civitai_app_app_blocks_storage_latency_seconds{op}` histogram — buckets 1ms → 2.5s, registered HMR-safe with the same pattern as the bitdex shadow-query histogram. - Every `apps.storage.*` procedure starts a timer on entry, ends it in `finally`. Failures + happy paths both observe the timing so error bursts are visible in latency dashboards (not just the counter). - Per-write audit on `set` (every success → `event: 'set'` with appBlockId / blockInstanceId / userId / key / sizeBytes / isInsert) and `delete` (only when a row was actually removed). Streams via the existing `logToAxiom` path under `app-storage-trpc` log name — Loki-side alerts can compute the >10/s sustained-write abuse signal the handoff calls out without us holding rate counters in-process. - Quota-exceeded log line was already in place from P3 — the histogram + per-write audit are the additions here. The hourly per-(app_block_id) used_bytes / row_count gauges from the handoff are deferred to a follow-on commit (probably a CronJob in datapacket-talos rather than per-pod polling). Counters + histogram + audit log are enough for the v0 acceptance gate; gauges are nice-to- have for the marketplace UI in W1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(apps): admin backfill endpoint for KV provisioning (W4-KV-v0 P7) `GET /api/admin/apps-storage-backfill?token=$WEBHOOK_TOKEN` — walks every `app_blocks.status='approved'` row, calls `AppStorageProvisioner.provision({ appBlockId, slug })`. Idempotent. Dry-run by default; pass `?apply=true` to actually provision. Use cases: - W2 webhook never fires (preview environment, manual SQL inserts): one-shot to bring the apps DB in sync. - cnpg-cluster-apps reset (DR, schema-explosion cleanup, etc.): re-provision everything in one call. - Manual one-off: `?appBlockId=apb_xxx&apply=true` targets a single app. - Recommended operator cron: 1h heartbeat against `?apply=true` until W1's submission queue lands — closes the gap between W2 webhook retries. 503s cleanly when `APPS_DATABASE_URL` is unset so PR previews that don't have the apps DB wired stay deployable without hitting this endpoint. Pairs with the v0 ship handoff in datapacket-talos (claudedocs/app-blocks-w4-kv-datastore-v0-shipped-2026-05-27.md) which documents the four operator preflight steps + the end-to-end smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): regen pnpm-lock.yaml for @civitai/app-sdk + @civitai/blocks-react W3 added the two SDK packages to package.json (commit bb0e98e95) but didn't regen the lockfile, so Tekton (which uses --frozen-lockfile) failed every preview build since W3 merged. Resolves to @civitai/app-sdk@0.6.0 + @civitai/blocks-react@0.4.1 from npm (0.4.1 is the workspace: leak patch). No other deps changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): add yaml package — required by W2 apps-pipeline.service W2's apps-pipeline.service.ts imports `* as YAML from 'yaml'` for envsubst-style manifest manipulation, but yaml wasn't declared in package.json. Tekton typecheck failed with TS2307. Adds yaml@^2.8.1 (already a transitive dep at that version; promoting to direct). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): verify HMAC over raw bytes, not re-serialized JSON (#2338) * fix(blocks): verify HMAC over raw request bytes, not re-serialized JSON Forgejo signs the pretty-printed Go-encoded body it sends, with `\n ` indentation. Next.js's bodyParser parses the JSON into req.body, and the handler's `JSON.stringify(req.body)` produces compact JSON with no whitespace — the byte sequences differ, so the HMAC never matched. Repro on civitai-pr-2319: every Forgejo push to civitai-apps/* logged `401 Bad signature` in Forgejo's hook_task table; receiver received the request and rejected it. Fix: disable Next's bodyParser on git-push.ts + build-callback.ts, read the raw stream into a Buffer, verify HMAC against the raw bytes, then JSON.parse for the handler's logic. Caps body size to bound surface. Same bug class as the Stripe / Coinbase / Paddle webhooks already in this repo — they all use `bodyParser: false` with a manual stream reader for this reason. Unblocks App Blocks W12 cutover gap #1. * fix(blocks): triggerBuild via HMAC trigger receiver, drop kubeconfig path W2-v0's design had civitai-web parse a kubeconfig and POST PipelineRuns directly to dc-02-a's API server. That doesn't work — dc-02-a's API is loopback-only (SSH-tunnel for operators) and not reachable from dp-1 pods. The kubeconfig also used cert-auth which the code couldn't parse. Replace with a small HMAC-protected receiver on dc-02-a (`app-blocks-trigger`, see datapacket-talos/claudedocs/app-blocks-tekton-trigger/). civitai-web POSTs JSON to the receiver via the existing dp-1 → dc-02-a VPN proxy (`wireguard-proxy-service:8088`), receiver validates HMAC, creates the PipelineRun with its own in-pod ServiceAccount. Trade-off: one more piece of cluster infra (small Python receiver, ~150 lines). But: no kubeconfig juggling, no dc-02-a API exposure, standard HTTP+HMAC pattern that's easier to debug than k8s API impedance mismatches. Env schema changes: - Removed APPS_TEKTON_KUBECONFIG, APPS_TEKTON_NAMESPACE - Added APPS_TEKTON_TRIGGER_URL, APPS_TEKTON_TRIGGER_SECRET Unblocks App Blocks W12 cutover gap #3. * chore: re-trigger pr-preview build (deploy-dev label was added post-merge) * chore: re-trigger pr-preview with preview-db/prod label (use prod DB for app_blocks) * chore: retrigger pr-preview after buildkit lock contention (792b6 failed) * feat(app-blocks): migrate to civit.ai domain (single-level wildcard) (#2340) * feat(app-blocks): migrate domain to civit.ai (single-level wildcard) `*.apps.civitaic.com` would be two levels under civitaic.com — CF Universal SSL is single-level wildcard only, so each per-app subdomain would need a paid Cloudflare Advanced Cert (~$10/mo each) OR DNS-only routing without CF's edge protection. Switching to `<slug>.civit.ai` (single-level on a dedicated zone) is covered by CF Universal SSL for free. Changes: - APPS_DOMAIN default: apps.civitaic.com → civit.ai - submit.tsx: replace 4 hardcoded refs (UI display strings) - apps-pipeline.service.ts: pass APPS_DOMAIN to apply Job env so the template ConfigMap can interpolate it into IngressRoute + Certificate - forgejo.service.ts: doc comment forgejo.civitaic.com → forgejo.civitai.com Paired with datapacket-talos changes (ExternalDNS domain-filter + app-templates ConfigMap + Forgejo IngressRoute migration to civit.ai + GitHub-org oauth2-auth gate on Forgejo). * fix(blocks): apply Job image bitnami/kubectl:1.34 → alpine/k8s:1.34.0 docker.io/bitnami/kubectl:<ver> returns NotFound (Bitnami images retired 2025-Q4). The apply Job stuck ImagePullBackOff after callback. Switch to alpine/k8s:1.34.0 which has bash + kubectl + envsubst — matches the template's `bash -c` + envsubst usage. * feat(blocks): auto-add per-app host to OauthClient.allowedOrigins on submitApp (#2344) Without this, the first Forgejo push for a newly-submitted app rejects the manifest with `400 iframe.src rejected: origin https://<slug>.<APPS_DOMAIN> not in OauthClient.allowedOrigins`. Operator had to manually run an UPDATE on the OauthClient row before the build pipeline could fire (bit twice during the W12 cutover). Read allowedOrigins on the existing OauthClient + append the new host if not already present. Idempotent (won't dup on resubmit). Skipped on unique-constraint conflict path (existing app block) so the conflict error surfaces clearly. * chore: retrigger pr-preview to pick up #2340 + #2344 * feat(blocks): /apps/submit can auto-create OauthClient inline Until now the form required picking an OauthClient owned by the submitter. `oauthClient.getAll` filters by `userId: ctx.user.id`, so a first-time moderator with zero owned clients hit a hard "No OAuth clients found" wall and had to drop into psql before the form would accept anything. The implicit shape was also off: each block should have its own OauthClient (allowedOrigins + scopes diverge per app), so the list-existing flow was implicitly encouraging client-reuse across unrelated blocks. Make oauthClientId optional in `blocks.submitApp`. When absent, insert a fresh public client scoped to this app: id <ulid>-app-block-<slug> (matches hackathon pattern) secret null (block iframes can't hold secrets) name description ?? "App Block: <slug>" redirectUris [] (App Blocks use the JWT path, not code flow) allowedOrigins [https://<slug>.<APPS_DOMAIN>] isConfidential false userId ctx.user.id The post-create allowedOrigins-append from #2344 becomes a no-op for auto-created clients. UI swaps the Select for a SegmentedControl ("Create new" / "Use existing"), defaulting to "Create new" and disabling "Use existing" when the user has zero owned clients. Both modes share the rest of the form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: retrigger pr-preview (95t8j hung at #33 cache export) * chore: retrigger pr-preview * chore: retrigger pr-preview * chore: retrigger pr-preview * chore: retrigger pr-preview * ci: retrigger pr-preview after replace --force deploy fix Verifies the fix for the env value/valueFrom merge break that failed pr-preview-t6t9g (APPS_TEKTON_TRIGGER_SECRET flip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 1+2 — publish-request flow backend Lays the substrate for the App Blocks W1 publish-request flow: dev uploads a ZIP via the UI, civitai-web stores it on ssd-minio-backups MinIO, computes manifest + file diff summaries vs the previous approved version, and inserts an app_block_publish_requests row for moderator review. Replaces the W12 direct-Forgejo-push UX (which exposed Forgejo to developers). Under W1, devs never see Forgejo: mod review happens in /apps/review (Phase 3), and on approve the platform uploads to Forgejo server-side and the existing Tekton build chain fires. Phase 1 (data model): - prisma/schema.full.prisma — AppBlockPublishRequest model with status enum, FK to AppBlock (nullable, populated on first approve), 4 composite indexes (mod queue, per-app history, my-submissions, slug lookup). - prisma/migrations/20260528170000_w1_publish_requests/migration.sql — manually applied to cnpg-cluster-nvme0 prod 2026-05-28 per CLAUDE.md gotcha #14. Includes 4 CHECK constraints (status enum, review-pair, rejection-reason-required, approved-forgejo-sha-required) and an updated_at trigger. Phase 2 (submit-version backend): - src/env/server-schema.ts — BUNDLE_S3_* env vars (optional so envs without W1 wiring still boot). - src/utils/bundle-s3.ts — S3Client + bucket getter for the ssd-minio-backups MinIO endpoint with bucket-scoped credentials. - src/server/schema/blocks/publish-request.schema.ts — submitVersion + withdrawRequest input shapes; 50 MiB bundle cap (67 MiB pre-decode base64 cap); 2000 files / 10 MiB-per-file in-bundle caps. - src/server/services/blocks/publish-request.service.ts — pipeline: decode bundle → parse ZIP (jszip) → hash each file → extract+validate manifest → look up previous approved version → compute file_summary and manifest_diff_summary → upload bundle to MinIO (idempotent on SHA) → insert publish_request row. Lazy-imports dbRead/dbWrite/newUlid so the pure helpers (extract, diff) are unit-testable without booting Prisma. - src/server/routers/blocks.router.ts — submitVersion mutation, withdrawPublishRequest mutation, listMyPublishRequests query. - src/server/services/blocks/__tests__/publish-request.service.test.ts — 19 deterministic tests covering computeFileDiff (add/remove/change + order independence), computeManifestDiff (first-version, scalars, deep-object hash, large-value summarisation), and extractBundleMetadata (valid bundle, missing manifest, empty, invalid JSON, deterministic hashes, sorted file list). Phase 3+ (deferred): mod review backend (approveRequest, rejectRequest, listPendingRequests) + /apps/review UI + /apps/submit redesign + /apps/my-submissions UI. Forgejo terminology purge in Phase 4. See claudedocs/app-blocks-w1-publish-request-flow-v0-handoff-2026-05-28.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 3 — mod review queue + approve/reject Closes the dev → mod → live loop for App Blocks publish requests. Mods hit /apps/review to see pending submissions, click into one to view the manifest + diff summary + file change counts, and approve or reject with a reason. On approve the platform pre-creates the OauthClient + app_blocks row (first version) and atomically commits the bundle to Forgejo in a single multi-file commit; the existing git-push webhook then takes over and fires the Tekton build chain. Backend: - forgejo.service.ts +commitFiles, +listRepoTree. commitFiles uses the Forgejo `/contents` multi-file endpoint so a full repo rewrite is one push event → one webhook fire → one build, not N. replaceAllFiles=true emits delete operations for repo files that aren't in the bundle so starter scaffolding doesn't linger after first-approve. - publish-request.service.ts +listPendingRequests, +approveRequest, +rejectRequest, +fetchAndExtractBundleFiles. approveRequest re-uses the auto-register OauthClient pattern from 5b304be53 (lifted from submitApp); first-version path also creates the Forgejo repo from the starter and sets up the push webhook. Pre-inserts app_blocks with status='approved' so the git-push handler (which does an UPDATE, not UPSERT) finds the row when the Forgejo commit fires. - publish-request.schema.ts +listPendingRequestsSchema +approveRequestSchema +rejectRequestSchema. Rejection reason 10-2000 chars (shown to dev verbatim on /apps/my-submissions). - blocks.router.ts wires three new mod-only procedures behind ctx.user.isModerator + enforceAppBlocksFlag. UI: - pages/apps/review.tsx — Mantine table of pending requests + Modal with manifest viewer, file-list diff, manifest-field diff (added/removed/changed with from/to values), approve / reject buttons. /apps/submit and /apps/installed terminology purge moves to Phase 4. Trust model: mods review the manifest + file list (counts + paths). Per-file source diffs deferred to Phase 4+; the build pipeline's container limits + CSP are the enforced sandbox. Phase 4 next: dev-facing /apps/submit redesign (drop the Forgejo / git-clone copy + the OauthClient picker shipped in 5b304be53), new /apps/[slug]/submit-version page, /apps/my-submissions page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 4 — dev-facing UI redesign + Forgejo purge Rewrites /apps/submit for the ZIP-upload publish-request flow and adds /apps/my-submissions for the dev's view of their submission history. /apps/submit: - Drops the OauthClient picker + SegmentedControl shipped in 5b304be53 (auto-create now happens server-side in approveRequest, Phase 3). - Drops the "Forgejo repo created" / "git clone" / "Clone URL" copy shipped in W2 — devs never see Forgejo under W1. - Drops the form name + description fields (Phase 2 decision: trust the manifest, no duplicate truth). - New form: slug + version + ZIP upload (FileInput, accept=".zip", 50 MiB cap visualized). - Browser reads file as base64 via FileReader.readAsDataURL, posts via blocks.submitVersion. Success card surfaces the publishRequestId + links to /apps/my-submissions. /apps/my-submissions (new): - Table of viewer's publish requests (newest first, from blocks.listMyPublishRequests). - Inline rejection-reason row beneath any rejected submission (red background, whitespace: pre-wrap). - Inline approval-notes row beneath approved submissions (green). - Withdraw button on pending; Open-live on approved (target=_blank); Resubmit on rejected. - Fragments wrapped with key={s.id} so the conditional reject/approve rows don't break React's array reconciliation. Terminology audit: /apps/installed, /apps/index, /apps/[appBlockId] all clean — no Forgejo/repo/git-clone copy to remove. The old blocks.submitApp endpoint (with the 5b304be53 auto-register code) is left in place — no longer the canonical path but still callable. Phase 6 cleanup removes it once Phase 5 backfills the existing generate-from-model app into the new publish_request table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(blocks): drop unused Anchor import in my-submissions * feat(blocks): W1 v0 Phase 5 — backfillPublishRequest for live apps Migration helper that reconstructs a publish_request row for an app whose first version predates the W1 flow. Pulls the current Forgejo state into an in-memory ZIP, uploads to MinIO, and inserts a status='approved' row linked to the existing app_blocks entry. Without this, the first real submitVersion against the live generate-from-model app would diff its bundle against nothing and report "+all files" as the change set. forgejo.service.ts: - export listRepoTree (was internal to commitFiles) - +getBlobContent — reads a single blob via /repos/{owner}/{repo}/git/blobs/{sha}, decodes base64, returns Buffer. publish-request.service.ts: - +backfillPublishRequest. Pipeline: lookup app_blocks → fetch repo metadata → recursive tree walk → parallel blob downloads (8 in flight) → JSZip rebuild with epoch dates for deterministic bundleSha256 → upload to MinIO → reuse extractBundleMetadata for path/sha/size semantics consistent with live submissions → INSERT publish_request status='approved' with synthetic first-version file_summary and manifest_diff_summary. - Idempotent: re-running with the same Forgejo HEAD returns the existing publish_request via the (slug, bundleSha256) lookup; no duplicate row, no duplicate MinIO put (overwrites identical bytes). - Owner attribution: submittedByUserId comes from OauthClient.userId of the live app; reviewedByUserId is the mod invoking the backfill; reviewedAt = now. Schema + router: - backfillPublishRequestSchema (slug + optional approvalNotes). - blocks.backfillPublishRequest mutation, guarded by isModerator + appBlocks Flipt flag. Phase 6 deferred: Discord notify, queue-depth metrics, remove the legacy blocks.submitApp endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 6 — Discord notify on new pending + remove legacy submitApp Discord: - publish-request.service.ts +notifyModsOfNewRequest. Posts an embed to DISCORD_WEBHOOK_MOD_ALERTS with slug, version, submitter, change summary (first-version vs +/~/- file counts), request ID, and a link to /apps/review. Fire-and-forget; 5s timeout; both inner fetch and outer wrapper catch — a Discord outage cannot block submitVersion. - submitVersion calls it via `void notifyModsOfNewRequest(...)` after the publish_request INSERT, with the submitter's username denormalized from a separate dbRead.user lookup. Legacy cleanup: - Removed blocks.submitApp procedure (~160 lines). Under W1 the developer-facing "create repo" step disappears entirely — OauthClient + Forgejo repo + app_blocks row are all created server-side in approveRequest (Phase 3) on first-version approve. The 5b304be53 auto-register SegmentedControl UI was already gone in Phase 4; this drops the now-orphan server endpoint. - Removed now-unused `newUlid` import from blocks.router.ts (every remaining use of newUlid lives inside the publish-request service). - Updated the dangling submitApp reference in git-push.ts:140 to describe the W1 pre-create-in-approve semantics. Skipped for v0: Prometheus metrics on queue depth + time-to-review. Mods can SQL-query app_block_publish_requests directly; revisit if the queue starts mod-fatiguing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(blocks): W1 v0 publish-request flow test coverage + audit follow-ups Adds vitest coverage for the W1 v0 publish-request orchestration layer (submitVersion, withdrawRequest, listPendingRequests, approveRequest, rejectRequest, backfillPublishRequest) plus the new forgejo.service helpers (listRepoTree, getBlobContent, commitFiles). 19 -> 82 tests on the App Blocks W1 surface. Boundary cases for the 50 MiB / 10 MiB / 2000-file caps. 13 regression tests labeled REGRESSION (C-1..C-4, H-1..H-4, M-4) lock in current behavior on findings from claudedocs/app-blocks-w1-v0-audit-2026-05-28.md so when the fixes land the assertions flip clearly. New test files: - src/server/services/blocks/__tests__/publish-request.orchestration.test.ts - src/server/services/blocks/__tests__/forgejo.service.test.ts Extended: - src/server/services/blocks/__tests__/publish-request.service.test.ts (+9 boundary + schema cap tests) No production code changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): C-1 + C-2 from W1 v0 audit C-1: tRPC bodyParser was capped at 17mb, silently 413-ing bundles >~12 MiB even though the schema cap (MAX_BUNDLE_SIZE_BYTES) and the UI both advertise 50 MiB. Raise to 72mb so a 50 MiB ZIP base64-encoded inside the tRPC envelope fits. Acknowledged: this widens the cap for every tRPC route. The v1+ migration path is a dedicated /api/internal/blocks/ upload-bundle route that isolates the cap to the bundle path. C-2: approveRequest's OauthClient.id was `${ulid()}-app-block-<slug>` (non-deterministic), so any retry after a mid-flow failure (e.g. Forgejo 500 on createRepoFromTemplate) generated a *different* OauthClient.id and silently piled up orphans across retries. With slug-derived deterministic id (`appblk-<slug>`): - Retry's oauthClient.create hits the PK unique constraint (P2002); catch + findUnique recovers the existing row instead of inserting a second one. - Two concurrent first-version approves for the same slug now collide at the OauthClient PK rather than each succeeding with distinct ids (incidentally blunts C-3 — see test FIX (C-3)). - Wrap appBlock.create in the same P2002 catch + findFirst recovery so the recovery is symmetric. The recovered row gets a manifest refresh to converge on the new state. Tests: - publish-request.orchestration.test.ts: - First-version happy path now asserts the deterministic id (ocArg.id === 'appblk-hello'). - REGRESSION (C-3) replaced by FIX (C-3): models two concurrent approvers, second hits P2002 on both creates, falls through to findUnique/findFirst, ends with one OauthClient + one AppBlock. - New FIX (C-2): retry after Forgejo 503 in attempt 1 successfully completes in attempt 2 without orphan accumulation. - New FIX (C-2): non-P2002 errors on oauthClient.create are surfaced rather than silently swallowed by the catch. - mockDbRead gained an `oauthClient.findUnique` mock to back the new recovery lookup. - All 84 W1 tests pass (45 orchestration + 28 service + 11 forgejo). The 4 baseline failures elsewhere (1 buzz-attribution + 3 checkpoint) are pre-existing and unrelated, per the audit's note. C-1 fix sketch (Option A from audit) ships; C-2 fix sketch (Option C — deterministic id) ships. C-3 is blunted as a side effect but the audit's full C-3 fix (`@@unique([blockId])` migration or SELECT FOR UPDATE) is deferred — the catch-based recovery is sound for v0 internal-team submission volumes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): C-3 + C-4 from W1 v0 audit — DB uniqueness constraints Adds two DB-level constraints that close the read-then-write race windows the C-2 OauthClient.id determinism couldn't reach: C-3: ALTER TABLE app_blocks ADD CONSTRAINT app_blocks_block_id_unique UNIQUE (block_id); The existing (app_id, block_id) constraint doesn't protect because each approve mints a fresh app_id (now deterministic per C-2, but a bare (block_id) constraint is the belt-and-suspenders). BlockRegistry and the JWT issuer both assume one app per slug; this is the DB-layer enforcement of that invariant. C-4: CREATE UNIQUE INDEX app_block_publish_requests_one_pending_per_slug ON app_block_publish_requests (slug) WHERE status='pending'; Closes the window between submitVersion's "no pending request?" findFirst and its INSERT. Partial index lets approved / rejected / withdrawn rows accumulate without conflict. Pre-flight verified clean on cnpg-cluster-nvme0 prod 2026-05-28: app_blocks duplicate block_ids: 0 rows publish_requests duplicate pending slugs: 0 rows Migration 20260528210000_w1_uniqueness_constraints applied manually. Service changes: - publish-request.service.ts submitVersion: wrap the INSERT in try/catch P2002. On collision, surface a human-readable error ("already has a pending publish request (race window); withdraw the other or retry") matching the app-layer check's message. - approveRequest's existing P2002 catch from the C-2 fix already handles AppBlock.create collisions on (block_id) — no code change there. Prisma schema: - AppBlock model gains @@unique([blockId], map: "app_blocks_block_id_unique") alongside the existing (appId, blockId) constraint. - The partial unique index on publish_requests is raw SQL (Prisma doesn't model partial unique constraints first-class on this version); generated client doesn't need to know — the runtime P2002 catch handles it. Tests: - REGRESSION (C-4) replaced by FIX (C-4): second concurrent submitVersion now throws the human-readable error after the partial-index P2002. - New FIX (C-4): non-P2002 errors on the INSERT are surfaced (not silently swallowed by the catch). - New FIX (C-3): AppBlock.create P2002 (block_id collision) falls through to findFirst + update existing — covers the case where C-2's OauthClient layer is bypassed but C-3's DB constraint still protects. - All 86 W1 tests pass (47 orchestration + 28 service + 11 forgejo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): derive slug + version from manifest, drop redundant form fields The submit form had separate slug + version TextInputs whose only purpose was to be cross-checked against manifest.blockId / manifest. version. A typo in either field produced a confusing "manifest blockId (hello-world) does not match form slug (hello-world-block)" error, when the right fix was to delete the form fields and trust the manifest as the source of truth. Schema: - submitVersionSchema drops slug + version; accepts only bundleBase64. Service: - submitVersion derives slug from manifest.blockId, version from manifest.version, name from manifest.name. Each gets a shape check (SLUG_REGEX, SEMVER_REGEX, non-empty) that surfaces a human-readable error before any S3 / DB writes happen. - Removed the form-vs-manifest cross-check branch. - Approve flow + downstream (commitFiles, app_blocks, OauthClient appblk-<slug>) all read slug via the existing `request.slug` path from the publish_request row — no change there. UI: - /apps/submit: drop slug + version TextInputs. On file pick, parse the ZIP client-side via jszip, extract block.manifest.json, validate the same shape rules as the server. Surface a preview card listing slug, version, name, description, contentRating, slots. Submit only enables when the preview parses cleanly — failures show inline with a clear message before the user round-trips. Tests: - 2 orchestration tests rewritten: "blockId does not match slug" → "blockId is not a valid slug" (sends `NotALowercaseSlug`); "version does not match" → "version is not valid semver" (sends `not-semver`). - All other submitVersion call sites had `slug:` + `version:` lines stripped (the params are no longer accepted by the function). Bundle helper's manifest defaults to blockId=hello / version=0.1.0 so behavior is preserved. - All 86 W1 tests pass (47 orchestration + 28 service + 11 forgejo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): push bundle to in-review Forgejo repo + link from /apps/review So mods can see the actual code, not just the manifest diff. On every submitVersion we ensure a per-slug repo exists in a new civitai-apps-review org and commit the bundle's files there with replaceAllFiles=true. The review modal gains a "View code in Forgejo" button that deep-links to the repo's tree view, where Forgejo's diff UI is what the user wanted in the first place. Design choices: - One repo per slug (civitai-apps-review/<slug>), overwritten on every submitVersion. Single submission visible at any time (the C-4 partial unique index already enforces one pending per slug). - Separate org from civitai-apps so the git-push webhook + Tekton don't fire on review pushes — the build chain still triggers only when approveRequest commits to the canonical civitai-apps/<slug>. - Push happens BEFORE the publish_request INSERT. If Forgejo is sick, the submission fails clean (bundle stays in MinIO, idempotent on SHA; no orphan DB row). - Repo creation is idempotent (422/409 = already exists, fine). ensureReviewRepo also creates the org on first call. forgejo.service.ts: - New FORGEJO_REVIEW_ORG = 'civitai-apps-review' constant. - New ensureReviewRepo(slug) — creates org (POST /api/v1/orgs) and per- slug repo (POST /api/v1/orgs/<org>/repos with auto_init=true) idempotently. - New exported reviewRepoUrl(slug) helper for the UI link. - listRepoTree + commitFiles gained an optional `org` parameter (defaults to FORGEJO_ORG so existing callers are unchanged). publish-request.service.ts: - submitVersion re-decodes the bundle in-memory after the MinIO PUT and pushes per-file contents to the review repo with replaceAllFiles=true. ~50ms overhead for a 50 MiB bundle on a modern node; cheaper than threading per-file contents through extractBundleMetadata's return. - listPendingRequests payload now includes reviewRepoUrl(slug) so the UI doesn't need to construct it. review.tsx: - PendingRequest gains reviewRepoUrl: string. - Modal renders a default-style "View code in Forgejo" button with IconCode + IconExternalLink, target=_blank. Tests: - mockForgejo gained ensureReviewRepo + reviewRepoUrl stubs. - All 86 W1 tests pass (47 orchestration + 28 service + 11 forgejo). Known follow-up: forgejo.civitai.com's oauth2-proxy currently requires GH `oauth` team membership, so mods clicking the link 403 until they're added to that team (or the gate is loosened to org-only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): make civitai-apps-review repos public for anonymous mod browsing Mods get a "Login Failed: Unable to find a valid CSRF token" error on forgejo.civitai.com's Forgejo-side login form (orthogonal cookie / CSRF issue; the oauth2-proxy gate itself works — Tekton dashboards behind the same gate load fine). Without a Forgejo login session, the per-slug review repo I just added (private by default) is unreadable from /apps/review's deep-link. Flip `private: true` → `private: false` so review-repo file trees are anonymously browsable inside Forgejo. The security boundary stays oauth2-proxy: every request to forgejo.civitai.com goes through the GH `oauth` team gate first; only inside that boundary does Forgejo serve the public view. Acceptable for these throwaway-per-submit review snapshots. Canonical civitai-apps repos stay private (built-and-deployed code; the git protocol path is open at `*.git` for Tekton clone auth, but the HTML browse view still needs a Forgejo session). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): use browser-facing FORGEJO_PUBLIC_URL for review-repo link reviewRepoUrl() was using getBaseUrl(), which returns FORGEJO_BASE_URL — the cluster-internal forgejo-http.forgejo.svc.cluster.local:3000 endpoint civitai-web uses for its API and webhook calls (avoids the Cloudflare + oauth2-proxy round-trip). The /apps/review modal link needs the BROWSER-facing URL. Add a new FORGEJO_PUBLIC_URL env (default `https://forgejo.civitai.com`) and have reviewRepoUrl read from it. FORGEJO_BASE_URL stays unchanged for all the other Forgejo service-layer calls (createRepo, commitFiles, listRepoTree, getBlobContent, etc.) which still want the in-cluster endpoint. Default works for prod + PR previews without any env wiring change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): inline-detect & replace existing pending submission on /apps/submit Resubmitting a bundle while a pending request already existed for the same slug surfaced a raw server error in a toast — even though the dev's intent was clearly to supersede their own pending row. Pre-flight the conflict during preview so we can offer a "withdraw and resubmit" affordance instead of letting the user submit into a guaranteed failure. - New blocks.getMyPendingForSlug query (own-rows-only) — surfaces an existing pending pubreq for the previewed slug. - /apps/submit fires it once manifest parses; renders a yellow Alert with the pending version + submittedAt + id and morphs the submit button to "Withdraw and resubmit". handleSubmit calls withdrawPublishRequest then submitVersion; withdraw failure short-circuits with a clean toast. - Sharpened the server-side same-slug error: same-user wording suggests self-withdrawal; other-user wording no longer leaks the conflicting pubreq id (useless to a non-owner). - Tests: split the existing same-slug rejection test into same-user vs other-user (asserts id is NOT in the other-user message); 3 new tests for getMyPendingForSlug (null when none, returns own row, where-clause scopes to caller). 79/79 publish-request tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): /apps/review history tabs for approved + rejected publish requests Adds Approved + Rejected tabs to the moderator review page so mods can browse the publish-request history with the inline approvalNotes / rejectionReason and the reviewer attribution surfaced. Active tab is mirrored to ?tab= for deep-linking. The review modal grows a read-only mode (no approve/reject buttons) for history rows and surfaces the mod feedback in a coloured callout. Backend: two new service functions (listApprovedRequests, listRejectedRequests) mirroring listPendingRequests, two tRPC procs wired through the same isModerator + enforceAppBlocksFlag gates. Schemas reuse the existing listPendingRequests cursor shape. Mod-history coverage in the orchestration test suite verifies status filtering, reviewedAt-desc ordering, inline notes, and cursor pagination (63 tests passing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W3 Phase 4 — manifest-driven settings in AppSettingsModal The install modal hardcoded two fields (buzz_budget_per_gen NumberInput, default_checkpoint_version_id picker) which made sense for one block but leak as a UX bug everywhere else: a viewer-identity block like who-am-i that has no generation surface still showed users a "Buzz budget per generation" control with no effect. Replace with a manifest-driven renderer reading block.manifest.settings through the same ManifestSettings type the server's validateBlockSettings already consumes. Fields render per the meta-schema's widget contract (number/string/boolean × text/textarea/select/toggle/resource_picker). Apps with no settings declaration get a clean modal with no "Block settings" divider. Apps that declare custom fields get those fields, no modal change required. - Pre-W3 manifests that omit per-field `scope` are coerced to 'publisher' for back-compat (gen-from-model is the prod case). - persistScope preserves unknown keys on existing subscription rows so legacy default_checkpoint_version_id values aren't dropped on resave — the platform's checkpoint resolution chain still consults them, and the right place to remove a field is the app's own manifest. - Mantine-native renderer (NumberInput / TextInput / Textarea / Select / Switch / picker Button) rather than the SDK's headless SettingsForm, which would clash with the modal's design language. Same widget contract; eventual W6 component pack will own the convergence. - Bonus copy fix: the modal Badge tracking subscription-target toggles read "No scopes selected" — conflated with manifest.scopes (JWT scopes). Renamed to "No targets selected" to keep the two concepts distinct in the UX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): readable reviewer notes + structured manifest view + install counts Three loosely-related UX fixes the W1 v0 dogfooding surfaced: 1. /apps/my-submissions reviewer notes were unreadable on dark theme — the rejected/approved feedback rows used --mantine-color-(red|green)-0 as a row background but the text inherited the default body color, which is light on dark theme → light-on-light. Replace the colored Table.Tr backgrounds with embedded Mantine Alert components which handle theme contrast natively (variant="light"). Same fix applied to the review-modal reviewer-history card (same root cause). 2. Add install counts to my-submissions table. New "Installs" column surfaces two compact pills per approved row: • ModelBlockInstall rows (per-model placements, blue, IconBox) • BlockUserSubscription rows (publisher + viewer scopes, grape, IconUsers) Pending-first-version + withdrawn-first-version rows have no AppBlock (FK populated on approve) so the cell renders "—". Backend: listMyPublishRequests now selects appBlock._count and flattens it onto the row as modelInstallCount + userSubscriptionCount. 3. /apps/review modal now parses the manifest into a structured view instead of dumping JSON.stringify into a ScrollArea. The new ManifestView renders five labelled cards: • Identity — name, blockId, version, content rating badge, trust tier, render mode, description body • JWT scopes — chips with human description per scope; unknown scopes flagged red (would fail at token issuance) • Slot targets — slot ids with priority + requiredContext list, human description per known slot • Iframe — src as link, sandbox flags as individual badges (allow-same-origin / allow-top-navigation / allow-popups-to-escape-sandbox flagged as higher-risk via orange + IconShieldLock + tooltip), dimensions, resizable • Settings — declared fields with type/widget badge, scope chip, requires_scope chip, label, description, default, range Anything outside the handled key set falls into an "Other manifest fields" Accordion with raw JSON so reviewers can still see unexpected payloads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): static manifest checks + apply-Job smoke test (catch broken apps before live) Two prevention layers for the class of bug that produced today's gen-from-model mixed-content incident: 1. Static iframe.src validation in submitVersion. Rejects at submit time (before mod review, before build, before deploy): - non-string / missing iframe.src - http: scheme (the obvious mixed-content trip) - hostname != "<blockId>.<APPS_DOMAIN>" — catches leftover hackathon URLs like https://blocks-pr2319.civitaic.com/<slug>/ - non-"/" pathname — catches the exact stale bundler-base + nginx redirect pattern that bit gen-from-model. Errors are human- readable and surface in /apps/submit as plain BAD_REQUEST. Tests: 7 new cases covering each rejection plus the canonical accept path. 2. Pre-flight smoke test inside the apply Job. Before kubectl apply touches the live Deployment: - kubectl run a Pod with the candidate image, runAsNonRoot+drop ALL+ RuntimeDefault (matches the namespace's PodSecurity:restricted) - wait for Ready (60s budget) - curl /healthz from the apply pod's container — must return 200 - curl / with one redirect hop — must return 200 + text/html - reject if any Location header in the chain embeds the in-pod port (:8080) — this is the precise signal that bit gen-from-model (nginx's redirect emitted $server_port, which Traefik proxied to the browser as http://<slug>.civit.ai:8080/, mixed-content-block) - EXIT trap cleans up the smoke pod even on script failure - Failure exits non-zero so the Job is marked Failed and the live Deployment is left untouched. Build chain surfaces the failure back through the existing job-watch. The script body is exported as buildApplyScript(ns) so we can pin its shape with orchestration tests in the future without restructuring the inline-args glue. RBAC follow-up (datapacket-talos commit, separate): apps-applier Role gains pods/log get verb so the smoke step can dump logs on Ready timeout. * fix(blocks): H-4 — validate manifest at approve time + Discord notify on webhook failure Two layered fixes for the class of "approved but never built" silent failure that produced 2026-05-29's gen-from-model incident: 1. H-4 fix (primary). approveRequest now runs the same BlockManifestValidator the git-push webhook runs, BEFORE any DB writes or the Forgejo commit. Without this fix the order was: a) approveRequest updates app_blocks.manifest in-place b) commitFiles fires the Forgejo webhook c) webhook 400s on the validator (e.g. sandbox flag "allow-popups-to-escape-sandbox" not allowed under trustTier=unverified) d) publish_request gets marked status='approved' anyway e) build chain silently never runs f) app_blocks row points at a manifest the live pod never serves With the fix, approveRequest rejects with a clear error that surfaces inline in /apps/review. The Forgejo commit, the four external system writes, and the publish_request status flip are all skipped. For first-version approves the OauthClient doesn't exist yet — approveRequest synthesises the AppContext it WOULD create (allowedScopes = Prisma schema default 33554431; allowedOrigins = [https://<slug>.<APPS_DOMAIN>]) so the validator runs on the same shape the webhook will see seconds later. For subsequent versions it reads the existing OauthClient via the AppBlock.app relation (existingAppBlock query extended to select the two fields). Two new orchestration tests assert the rejection wiring: - subsequent-version reject: no AppBlock update, no Forgejo commit, no S3 read, no publish_request update. - first-version reject: no OauthClient create, no Forgejo repo create, no webhook setup, no AppBlock create, no commit, no publish_request update. The previous REGRESSION (H-4) test (asserting the broken legacy behavior) is flipped into a FIX (H-4) test that asserts the new rejection path. Default test fixture grew contentRating + scopes + iframe.{maxHeight, resizable, sandbox} so existing happy-path tests still pass the stricter validator. 71/71 publish-request orchestration tests green. 2. Defense in depth: Discord notify on every webhook failure path. git-push.ts gains notifyModsOfWebhookFailure, wired into all five failure paths (fetch-manifest, parse-manifest, manifest-validation, blockId-slug-mismatch, iframe-src-mismatch, trigger-build). After H-4 this should fire only on direct Forgejo pushes that bypassed the approve flow, or if the approve-side and webhook-side validators ever drift. Fire-and-forget, 5s timeout, no-op when DISCORD_WEBHOOK_MOD_ALERTS is unset. Same payload shape as the existing notifyModsOfNewRequest helper. * fix(blocks): smoke-test pod needs imagePullSecrets for private ghcr images First real run of the pre-flight smoke step in the apply Job (2026-05-30 13:14 UTC, gen-from-model v0.2.1) sat in ImagePullBackOff: failed to pull and unpack image "ghcr.io/civitai/app-block-generate-from-model:81b5fe3b...": failed to authorize: 401 Unauthorized Block-app images are pushed to private ghcr repos. The main Deployment template propagates imagePullSecrets: [ghcr-cred] from the per-app manifest, but my `kubectl run --overrides` for the smoke pod didn't include it — the override JSON only set securityContext + automountServiceAccountToken + container shape. Add imagePullSecrets: [{name: ghcr-cred}] to the pod spec overrides. The ghcr-cred Secret already exists in civitai-apps (referenced by every per-app Deployment) so this is a one-line fix; no new RBAC, no new Secret. The stuck v0.2.1 apply was manually unblocked by deleting the failing Job + smoke pod and setting deploy/generate-from-model's image directly to the Tekton-built tag. Subsequent builds will exercise this fixed smoke step end-to-end. * feat(blocks): W8 multi-install tabs in BlockSlotClient Multi-install slots now render as Mantine Tabs ordered by manifest priority desc (per slot) then name asc. Single-install path is unchanged. Only the active tab's BlockHost is mounted at a time — inactive installs don't issue JWT tokens (cost + audit noise). Ordering logic extracted to a pure sortInstallsForSlot helper with 19 unit tests covering priority fallbacks, slot-specific priority lookup, name tiebreaker, malformed-target defense, and immutability. * feat(blocks): W5 v0 scope-reflection + activity feed on /apps/installed Adds two read-only reflection surfaces so users can see (a) what each installed app can request and (b) what apps have actually done on their behalf. v0 is deliberately a reflection layer, not a consent layer — the W5 grant schema is v1 work. Backend: two new tRPC queries on blocksRouter, both flag-gated like the rest of the file. - listMyScopeGrants aggregates per-app from enabled model_block_installs + block_user_subscriptions (one row per AppBlock, dedup'd). - listMyAppActivity is a cursor-paginated walk of block_buzz_attribution filtered by userId = ctx.user.id, ordered by attributedAt desc + id tiebreak. Cap 100, limit+1 trailing-row pagination pattern. Frontend: extends /apps/installed with a Mantine Tabs section ('Subscriptions' / 'Apps & permissions' / 'Recent activity'). Refactor: SCOPE_DESCRIPTIONS + SLOT_DESCRIPTIONS extracted from review.tsx to ~/server/services/blocks/scope-descriptions.constants so both pages share the same friendly-description source of truth. 23 orchestration tests for the service (aggregation, dedup, sort, scope fallback, cursor pagination, limit cap, user filtering). * feat(blocks): W5 v0.5 — version pin, uninstall, scope audit log on /apps/installed Adds the three /apps/installed extensions: 1. Per-install version pin (model_block_installs.pinned_version). NULL = follow latest approved release; a semver string = stored preference for that version's manifest. Wired so W2-v1 multi-version hosting can route on this column without another migration. tRPC: blocks.setInstallPinnedVersion validates ownership + approval status. 2. Uninstall button (UI). The existing blocks.uninstallFromModel tRPC already covers the data path; the Model installs tab adds the confirm-modal + invalidates listForModel for the affected modelId. 3. Scope-invocation audit log (block_scope_invocations). One row per scope-gated API call, written from block-scope.middleware.ts on res.on('finish'). Fire-and-forget — never blocks the response. /apps/installed Activity tab now interleaves Buzz attribution + scope invocations on a single timeline. UI: - New "Model installs" tab between Subscriptions and Apps & permissions. Each row: app + model link + slot + version Select + uninstall icon. Version dropdown shows "Latest (<currentVersion>)" + every approved version newest-first. - Activity tab gains an interleaved feed; status badge renders 2xx green, 3xx blue, 4xx orange, 5xx red for scope rows. Backend: - ALTER TABLE model_block_installs ADD COLUMN pinned_version TEXT. - CREATE TABLE block_scope_invocations + (user_id, invoked_at DESC, id DESC) index for the per-user feed + (app_block_id, invoked_at DESC) for the future per-app drill-down. - SignBlockTokenInput gains appBlockId so middleware logs don't need a per-request DB lookup; claim is required in BlockTokenClaims (strict shape check at verify time). Issuance passes block.id (the apb_<ulid>). Tests: +19 orchestration tests on user-app-surface (listMyModelInstalls batch-version-lookup, sort, ownership check, cursor coercion, BigInt→string serialisation, db-error swallowing). Existing block-token.service.test + block-scope.middleware.test updated to pass the new appBlockId field. Migration applied manually to prod cnpg-cluster-nvme0 per gotcha #14; backwards-compatible (additive column + new table, no data backfill). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W11 dynamic origin allowlist + gotcha #39 apply-wait fix Two unrelated-looking changes that ship together because they're both load-bearing followups from the W5 v0.5 session. W11 — dynamic CORS allowlist from OauthClient.allowedOrigins: block-scope.middleware.ts's allowlist is now the UNION of (a) BLOCK_ALLOWED_ORIGINS env CSV (kept as a transition shim), and (b) every approved OauthClient row's allowedOrigins[] column — populated automatically by the W1 approve handler. In-memory cache, 60s TTL, single-flight refresh on miss. Source becomes async (originAllowed + setBlockCors return Promise<>); withBlockScope already async so the only behavior shift is sub-millisecond cache hits in steady state + ~one DB round trip per pod per minute. Eliminates the per-new-block 3-yaml SOPS edit + rollout dance that gated every new block subdomain. The OauthClient row is the canonical source; the env CSV exists only for pre-W1 hackathon-era rows. Dynamic-imports dbRead inside loadAllowedOrigins so this module stays load-time side-effect-free (lets test envs import it without a full Prisma init). Same trick applied to the recordScopeInvocation call from the W5 v0.5 logging path — the eager import was dragging user-app-surface.service into module init. Gotcha #39 — defer current_version_deployed_at write to apply success: Before: build-callback set app_blocks.current_version_deployed_at the moment triggerApply returned (i.e. as soon as the Job was created, BEFORE the smoke step + kubectl apply + rollout-status). A failed apply (smoke/perms/NP — see today's v0.2.2/0.2.3/0.2.4 trap chain) left the column saying "deployed at <now>" while the live Deployment sat on the previous image. Now: build-callback responds 200 to Tekton immediately (the build's handoff to apply is its job; Tekton doesn't care about apply outcome), then a fire-and-forget watcher polls the apply Job (6 min ceiling, 5s ticks) until Succeeded / Failed / timeout. Only on Succeeded does the column flip + commit status go green; on Failed/timeout the column keeps its previous value (correctly reflecting the LAST successful deploy) and commit status flips red. New helper waitForApplyJob in apps-pipeline.service.ts polls the Job via .status.succeeded + Failed conditions (avoid .status.failed which counts attempts mid-backoff). Reusable for future per-app rollout status surfaces. Pod restart loses the watch handle — the column self-heals on the next successful build. Acceptable for v0; v1 polish would persist the watch via a CronJob reconciler or a Job-finalizer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(apps): add "My installed apps" link to /apps header Closes the one-way navigation gap: /apps/installed already links to /apps ("Browse the marketplace"), but /apps had no reverse path back to where users manage what they've subscribed to / installed. After a user subscribes from the marketplace they had no in-page affordance for "where do I see this now?" Always rendered (not gated like the mod-only SubmitAppLink) since the target page handles anonymous → /login redirect itself. Icon matches the Subscriptions tab on /apps/installed (IconPlugConnected) for visual consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): clarify Subscriptions vs Per-model installs on /apps/installed A user with multiple subscriptions but no per-model-pinned installs hit the Model installs tab and saw it empty, then assumed it was buggy. The tab name and empty-state copy didn't distinguish between two distinct install paths in App Blocks: - Subscriptions (block_user_subscriptions): "this app on ALL my models" / "on EVERY model page I visit" — covered by the existing Subscriptions tab. - Per-model installs (model_block_installs): "this app pinned to THIS specific model" — the affordance for one-off, model-specific placement. Changes: - Rename tab label "Model installs" → "Per-model installs" so it's clear from the chip alone that this isn't where Subscriptions live. - Header copy explicitly contrasts the two paths and points to Subscriptions for the blanket case. - Empty-state copy bridges back to the Subscriptions tab so a user who subscribed (and assumed they "installed") finds where their rows actually are. - Activity tab empty-state also gets a real explanation: it only populates on Buzz purchase from inside a block (openPurchaseModal attribution) or scope-gated REST API calls — NOT for vanilla generations that spend existing balance. Stops the "I generated stuff, why is this empty?" confusion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(blocks): kill per-model installs — absorb into block_user_subscriptions Deprecates `model_block_installs` as a user-facing concept and folds the per-model install primitive into `block_user_subscriptions`. One install surface instead of two; the data model now expresses pinning as a subscription with slot_id + target_model_ids[] populated. Why: - Real users don't distinguish "pinned to one model" from "blanket on all my models" — both are "install this app." Carrying two surfaces meant two tRPC procs, two /apps/installed tabs, two mental models. - block_user_subscriptions already had target_model_types[] + target_base_models[]; target_model_ids[] is the natural extension that lets it cover the per-model case too. The three filters AND together at listForModel time. Schema (migration 20260530210000_kill_per_model_installs, applied to prod cnpg-cluster-nvme0): - block_user_subscriptions: ADD target_model_ids INT[], slot_id TEXT, pinned_version TEXT, block_instance_id TEXT UNIQUE, installed_by _user_id INT. - Drop the (user_id, app_block_id, scope) UNIQUE; replace with two partial unique indexes — one for blanket subs, one for pinned subs. - block_user_settings FK repointed from model_block_installs to block_user_subscriptions (the bki_* id is preserved across migration for the one row that existed in prod). - DROP TABLE model_block_installs CASCADE. Code: - BlockRegistry.listForModel: rank-1 SQL branch is now the pinned-sub shape (slot_id non-NULL + target_model_ids contains modelId). Rank-2 is the blanket publisher-sub shape with slot_id IS NULL + empty target_model_ids. NOT EXISTS suppression checks the pinned-sub shape regardless of enabled — preserves publisher opt-out semantics. - BlockRegistry.resolveBlockInstance: mbi_*/bki_* prefix now looks up in block_user_subscriptions via the preserved block_instance_id column. Adds defense-in-depth Model.userId === bus.userId check. - BlockRegistry.installOnModel / uninstallFromModel / toggleEnabled / updateSettings: rewritten to operate on the pinned subscription shape. installOnModel uses findFirst+create/update (Prisma can't express the partial UNIQUE inline). - BlockRegistry.upsertSubscription: kept as the BLANKET-only write path. Pinning goes through installOnModel. - BlockRegistry.listUserSubscriptions: returns new fields (targetModel Ids, slotId, pinnedVersion, blockInstanceId, currentVersion, available Versions, pinnedModelNames) so /apps/installed can render version selector + uninstall + "Pinned to: <ModelName>" badges off one query. tRPC: - DROP blocks.listMyModelInstalls and blocks.setInstallPinnedVersion. - ADD blocks.setSubscriptionPinnedVersion (keyed on subscription id). - installOnModel / uninstallFromModel / toggleEnabled wire shape unchanged — iframe SDK callers (block-tokens, IframeHost) keep working transparently. - blocks.listMyPublishRequests: install count comes from a groupBy on pinned subscriptions (was the _count relation on the removed table). UI: - /apps/installed: drop the "Per-model installs" tab + ModelInstallsPanel. SubscriptionRow now handles both shapes — renders pinnedModelNames as small "Pinned to: <ModelName>" badges, exposes version Select + uninstall button when isPinned. Toggle uses toggleEnabled for pinned subs (preserving rank-1 NOT EXISTS suppression) and upsertSubscription for blanket. Downstream: - workflow-completed.ts + testing/blocks.ts: swap modelBlockInstall .findUnique → blockUserSubscription.findUnique by blockInstanceId. - PublisherSubscriptionBanner: unchanged. Its opt-out path (install + toggle disable) still works because installOnModel now creates a pinned subscription that suppresses the blanket at rank-1. After migration: the single live row (mbi_01KSD3NP23EQHXEPQRH32EX72G on model 2522512) became bus_pin_01KSD3NP23EQHXEPQRH32EX72G with its original bki_01KSD3NP23DEQQN4T264GFN3RH preserved. The block on generate-from-model.civit.ai's sidebar continues to resolve through the same blockInstanceId — block_buzz_attribution + block_user_ settings rows keep their referent. Tests: 102/102 in the directly-modified files (block-registry × 4, user-app-surface.orchestration). The previously-failing M2 install settings test in block-registry.service.test.ts now passes (replaced the modelBlockInstall.upsert mock with the new findFirst+update path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): kill_per_model_installs typecheck — mock signatures + listMyPublishRequests inference Two follow-ups to f3890bbd2 ("kill per-model installs") to clear the new tsc errors Tekton's pr-preview would catch: - block-registry.service.test.ts: vi.fn(async () => []) infers the return type as Promise<never[]>, which then rejects every .mockResolvedValue([{appBlockId: 'ab_one'}, ...]) call site with "Type {appBlockId: string} is not assignable to type never". Widened each hoisted mock to an explicit `(..._a: unknown[]) => Promise<unknown|unknown[]>` signature so mock.calls + mockResolvedValue both type cleanly. - blocks.router.ts listMyPublishRequests: rows.map((r) => r.appBlock?.id) had implicit `any` on `r` after the agent's flatten extraction. Bound RawRow via `(typeof rows)[number]` + typed the filter narrowing. No behavior change. 18/18 block-registry.service tests + 42/42 user-app- surface tests + 9/10 block-scope middleware tests pass (the 1 failure is the pre-existing baseline path bug at src/server/middleware/__tests__/block-scope.middleware.test.ts:113 — unrelated to this commit chain). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): log workflow submissions to Activity feed The Activity tab was populated only by: - block_buzz_attribution rows (Buzz PURCHASES from inside a block via openPurchaseModal — publisher revenue share) - block_scope_invocations rows (scope-gated REST calls via the JWT bearer middleware) Vanilla generations spending existing Buzz balance hit NEITHER path, so a user who ran "Generate" 10 times saw an empty Activity tab and reasonably wondered if it was buggy. Fix: piggyback on the existing block_scope_invocations table. After blocks.submitWorkflow's orchestrator call returns, fire-and-forget a recordScopeInvocation row with scope='ai:write:budgeted' + a synthetic endpoint='workflow:submit:<workflowId>' (the path is tRPC, not REST, so the endpoint string is synthetic). statusCode maps from snapshot.status to 200/500 so the existing color-coded badge keeps working. UI: humaniseScopeInvocation special-cases the workflow:submit prefix to render "Generated an image" instead of the generic "Submit AI workflow" scope label. The Detail column strips the synthetic prefix to show just the workflowId. Empty-state copy updated since vanilla generations now DO populate the feed. No schema change — reuses the existing block_scope_invocations table. Historical generations are not backfilled (no source data). Future generations from this commit forward will appear in the Activity feed ~immediately after the orchestrator submit returns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): audit hooks on remaining bridge-callable mutations + W4 unify After a54d56aff covered submitWorkflow, the Activity feed still missed: - updateUserSettings (viewer settings writes from the bridge, incl. SET_CHECKPOINT pin swaps) - apps.storage.set (W4 KV write) - apps.storage.delete (W4 KV delete) Each gap = an app action the user couldn't see in their audit trail. W4 already had a per-write logToAxiom call, but that surface is ops-only — not visible on /apps/installed. Fix: same shape as the workflow fix. Each mutation calls recordScopeInvocation post-success with a synthetic endpoint string: - user-settings:write (block:settings:write scope) - storage:set:<key> (apps:storage scope, new) - storage:delete:<key> (apps:storage scope, new, only on actual deletion — no-op deletes shouldn't pollute the feed) UI: - humaniseScopeInvocation: special-cased verbs for each pattern ("Saved your block settings", "Wrote app-local storage", "Deleted app-local storage") - humaniseScopeEndpoint: strips synthetic prefixes — workflow id, storage key, etc. surface clean in the Detail column W4 unify: NO new table — the existing logToAxiom call stays (ops/debug visibility) and the new recordScopeInvocation call populates the user-facing audit feed. Single SOT per surface, no double-bookkeeping. Out of scope (intentional): - estimateWorkflow / pollWorkflow — noisy (runs every page load / every 2s during a generation). Adds no signal. - install / uninstall / subscribe / unsubscribe / toggleEnabled — these are USER actions from platform UI, not actions apps take on the user's behalf. They don't belong in this feed. - OPEN_BUZZ_PURCHASE "ask" event — postMessage is client-side only; the COMPLETED buy already writes block_buzz_attribution. The "ask" without a completion is marginal value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): escape */ in BlockUserSubscription doc comment breaking prisma generate The blockInstanceId doc comment ended in `bus_pub_*/bus_view_*`. Prisma emits /// comments as JSDoc /** */ blocks in the generated client index.d.ts, so the `*/` closed the comment early and spilled text into TS, producing a corrupt .d.ts (TS1161 unterminated regex literal). This failed the Tekton typecheck on every commit since f3890bbd2 (kill_per_model_installs), so no deployable image built — leaving PR-2319 on the pre-migration f0dfd2980 image against a prod DB that had already dropped model_block_installs. Add a space (`bus_pub_* / bus_view_*`) to break the */ adjacency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview build (prior run hit buildkit lock contention) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(blocks): W7 host-rendered trust frame around app blocks (IframeHost) App blocks now render inside a host-controlled frame: a bordered container with a top chrome bar (Civitai "App block" badge + a menu whose "Manage apps" item links to /apps/installed). Rendered in civitai-web AROUND the iframe, NOT inside it — so a sandboxed third-party block can't fake, restyle, or hide the signal. This is the safety affordance that lets users distinguish app blocks from native Civitai UI. Always present during loading + ready. Mantine 7 (Group/Menu/ActionIcon), NextLink for the route. Per-file LSP typecheck clean; AppBlocks vitest green. Note: an earlier attempt put this inside the generate-from-model iframe (block v0.2.7) — wrong layer (spoofable). That in-iframe bar is being removed; this host frame supersedes it. * feat(blocks): unify /apps/installed into one-row-per-app list (surfaces as per-install setting) Collapse the Installs tab's two scope-split sections ("On models I own" / "On model pages I view") into a single list with one card per installed app. Blanket publisher/viewer subs become a "Shows on" badge summary (each with a location+audience tooltip); pinned per-model installs render in a subsection with the version Select + Uninstall controls preserved. Both-surface toggling still goes through the existing AppSettingsModal via Manage. Adds groupSubscriptionsByApp() pure helper + node-env vitest coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (sharp native-install flake, not code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): move groupSubscriptionsByApp helper+test out of src/pages/ The helper + its vitest file were under src/pages/apps/, so Next.js (default pageExtensions) tried to compile them as routes — webpack then followed the test's `vitest` import and failed on `node:module`, breaking the production build (typecheck passed; build-image failed). Moved both to src/components/Apps/ (next to AppSettingsModal) and repointed the import. No logic change; 6 grouping tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger (sharp/libvips native-install flake on build node) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): trust tier is moderator-controlled, not publisher-self-declared (C1) approveRequest read `trustTier` verbatim from the publisher manifest and defaulted a missing value to `internal` — the MOST privileged tier, which grants `allow-same-origin` (sandbox escape). A third-party manifest could self-escalate by declaring `"trustTier":"internal"` or omitting it. Fix: resolvedTrustTier = existingAppBlock?.trustTier ?? 'unverified'. The manifest's trustTier is normalised to the resolved value before the BlockManifestValidator call (it reads manifest.trustTier to gate the sandbox allowlist) and persisted at all 3 approve sites. Trust tier is now only raisable by a deliberate out-of-band moderator/DB action on the trust_tier column — never a manifest field. New apps default `unverified`; existing apps keep their current tier on re-approve (the 3 live internal blocks are all first-party and unaffected). Runtime sandbox reads the trust_tier column, so this closes the runtime self-escalation. approveRequest is DB/MinIO/Forgejo-coupled and has no unit harness (only the pure extract/diff helpers are tested); change is verified by typecheck. Part of the 2026-05-31 design-gap scan (C1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): marketplace install_count = distinct users, not subscription rows (M3) listAvailable counted block_user_subscriptions rows per app, so one user holding several rows for an app (blanket publisher + blanket viewer + N pinned-to-model subs after kill_per_model_installs) inflated the count — a pin-happy publisher could rank their own app higher. COUNT(DISTINCT user_id) makes install_count mean "distinct users." Marketplace ranking/display only; does not touch the listForModel rendering path. Part of the 2026-05-31 design-gap scan (M3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (ghcr push network timeout, not code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(blocks): behavioral harness for listForModel — real SQL on PGlite Adds an in-process Postgres (PGlite, Postgres-in-WASM) harness that executes the unmodified listForModel UNION-ALL query, so the install-model resolution bugs (H2, H2b) can be driven test-first. The existing block-registry test only asserts on the SQL string shape and mocks $queryRaw to return [] — it cannot catch behavioral bugs. - @electric-sql/pglite devDependency (executes @>, = ANY, cardinality, array_length — all PG-only operators the query uses). - listForModel.harness.ts: PGlite-backed $queryRaw bridge + schema + seed helpers (only the columns the query reads). - listForModel.behavior.test.ts: 12 green precedence/happy-path tests locking current correct behavior, plus 3 it.fails tripwires documenting the bugs. Empirical findings (real query run): - H2 REPRODUCES (rank-2 blanket + rank-3 default): a type-filtered pinned row that does NOT apply to the model still suppresses the fallback → blank slot. The NOT EXISTS suppressors match on (scope, slot, app_block, modelId) but do not re-check the pinned row's own target_model_types/target_base_models. - H2b REPRODUCES only in the SAME-app shape: an x-rated pin survives the SQL but is dropped by the JS content-rating filter, while the same-app platform default it suppressed in SQL is already gone → empty slot. A DIFFERENT-app fallback is NOT affected (suppressor is keyed on app_block_id) — kept as a green control test. Service code unchanged — fixes are a separate follow-up; flip each it.fails to it() once the suppressor re-checks pinned filters / rating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): H2 — pinned-sub suppressor must honour the pin's own type/base filters listForModel's rank-2/3/4 NOT EXISTS suppressors (and the rank-1 pinned SELECT) matched a pinned subscription on (scope, slot_id, app_block_id, modelId ∈ target_model_ids) but never re-checked the pin's own target_model_types / target_base_models. So a pinned row whose filters EXCLUDE this model still suppressed the blanket sub + platform default → blank slot where the publisher's app should show. Latent today (installOnModel writes empty filters) but the schema permits filtered pins. Fix: apply the same type/base predicate the blanket subs use to the rank-1 pinned SELECT and all three suppressor subqueries, so a non-applicable pin neither renders nor suppresses. Proven against the new listForModel PGlite harness — the two H2 tripwires flip green; precedence + opt-out invariants unchanged. 44/44 across the block-registry suites. H2b (content-rating) re-analysed and confirmed NOT a bug: suppressors are keyed on app_block_id, so a content-dropped pin only blanks its own same-rated fallback — an empty slot is correct. Harness test corrected from an it.fails to a green assertion documenting that. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (sharp fix now live in npm-typecheck task) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): W7 frame wraps error/timeout/fatal states too (FRAME-1) The host trust frame (AppBlockChrome) was rendered only in the success branch; the timeout/fatal/no_token/bad-src early returns dropped it, so a block could shed the "App block" provenance chrome + the "Manage apps" escape hatch by never sending BLOCK_READY (→ timeout) or sending BLOCK_ERROR{fatal}. Route every state through a `framed()` helper so the host chrome is present whenever the slot is occupied, including failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): FIN-1 — server-side re-validate buzz revenue attribution App Blocks buzz revenue attribution was client-forgeable end-to-end. The browser stamps blockAppId/blockAppBlockId/blockInstanceId/blockScope/ blockModelId + metadata.userId into the Stripe PaymentIntent metadata; getPaymentIntent copied it verbatim to Stripe and the webhook credited a publisher's revenue share off those fields. The only control was an isSelfPurchase check, defeatable by a 2-account ring: any authed user could assert a confederate's app + viewer_personal (25%) scope and mint fake publisher earnings on a purchase that never touched a block. Add a server-side chokepoint in getPaymentIntent (the last place holding the authenticated tRPC session) that re-validates / re-derives every block-attribution field against ctx.user.id before the PaymentIntent reaches Stripe: - spender: force metadata.userId to the session user; reject on mismatch - install existence: resolve via BlockRegistry.resolveBlockInstance as the session user (viewerUserId = ctx.user.id, db='write'); null -> STRIP all block fields so the purchase proceeds as a normal un-attributed buzz buy (never hard-reject a real-money purchase over a bad attribution) - scope: re-derived from the resolved instance's source, not client input - app: overwritten with the resolved install's app_id / app_block_id Carry slotId through the attribution wire shape (client-supplied + untrusted) so the resolver — which needs (modelId, slotId) — can re-validate; a forged slot simply fails to resolve and is stripped. Non-block purchases are unchanged passthrough. 11 node-env vitest cases cover all four forge vectors + legit + non-block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (sharp compile toolchain complete: +pkg-config) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(blocks): PAYOUT-1 safety substrate — hold gate, idempotent mint, refund clawback Makes the block_buzz_attribution financial state machine safe to switch on WITHOUT building real money disbursement (still leadership-gated, gotcha #26). Three invariants from the 2026-05-31 design-gap scan: 1. confirm-pending hold gate: replaces the blanket pending->confirmed updateMany with a per-owner velocity/volume circuit-breaker. Owners whose aging batch exceeds HOLD_VELOCITY_COUNT (200) or HOLD_VELOCITY_CENTS ($1,000) in a sweep are parked status='held' for manual review instead of auto-ripening. Idempotent (only touches status='pending'); disjoint held/confirm writes via notIn. 2. Idempotent payout mint: new block_attribution_payout ledger table with UNIQUE(app_owner_user_id, period_key) — a publisher is paid at most once per period. mintPayoutForOwner() inserts the ledger row + flips contributing confirmed rows to paid_out in one transaction; P2002 on the UNIQUE is an idempotent no-op. Net<=0 carries the debt forward (no mint, no flip). NOT wired into the bulk-payout cron — disbursement stays a logging stub. 3. Refund-after-payout clawback: voidAttributionsForPayment now writes a NEGATIVE carry-forward entry_type='clawback' row (status='confirmed') for each previously-paid_out row, so the payout aggregator nets the debt out of the publisher's next period. Clawbacks are written BEFORE the void so a mid-flight crash is crash-safe + idempotent (synthetic-key P2002 dedups repeat refunds) — voiding first would lose the debt on retry. Schema: +hold_reason/held_at/entry_type on block_buzz_attribution, the non-negativity CHECK scoped to entry_type='purchase' + a clawback-non-positive mirror CHECK (conservation CHECK unchanged: 0+0+(-X) = -X). Migration written, NOT applied — needs manual application to prod cnpg-nvme0 (gotcha #14). Tests: 38 passing across confirm-pending / buzz-attribution / rate-card suites (hold gate, mint idempotency + carry-forward, clawback ordering/dedup/ conservation). Also corrected a pre-existing stale v1->v2 rate-card assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): annotate mintPayoutForOwner txn callback return type (PAYOUT-1 typecheck) The $transaction callback's three return literals inferred `minted: boolean` (no contextual type), so the inferred union didn't match the MintPayoutResult discriminated union → tsc TS2322 (Tekton pr-preview-s49kj typecheck red). Annotate the callback `: Promise<MintPayoutResult>` so each return is contextually typed and the `minted: true|false` discriminants are preserved. Tests unchanged (21 passing); Serena per-file diagnostics clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): drop "App block" wordmark from host chrome bar (keep icon) The AppBlockChrome bar showed an IconApps + an "App block" uppercase wordmark. The icon + the frame already signal provenance, so the text is redundant — remove the <Text> (and the now-unused Text import). Added aria-label="App block" to the icon so screen readers keep the provenance signal. The dropdown's Menu.Label + menu aria-label are unchanged. * fix(app-blocks): server-seed slot reservation to kill model-page CLS Two stacked layout-shift causes on model pages when the App Block slot loads: Source A — BlockSlotClient returned null while blocks.listForModel was in flight, so the slot was 0px then popped to full height once the frame mounted, shoving sidebar content down. Source B — IframeHost swapped a hidden (display:none) iframe for a shown one on BLOCK_READY, a second jump when content height != minHeight. Fix: - SSR-prefetch blocks.listForModel on the model page (least-invasive: one source of truth, no new query) so the client useQuery hydrates with isLoading already false — no 0px flash. Input matches useBlockSlot's exactly so the React Query cache keys line up. - Add pure computeSlotReservation + CHROME_BAR_PX (35px, derived from AppBlockChrome) in a client-safe slotReservation module, re-exported by block-registry.service as BlockRegistry.getSlotReservation (reuses listForModel verbatim — cached/indexed, no N+1). - BlockSlotClient reserves a minHeight placeholder during loading ONLY when reservedHeight > 0; zero-install pages still return null (no dead gap). - useBlockSlot keeps previous data across refetch so the reserve doesn't collapse mid-refetch. - IframeHost renders the iframe visible-but-non-interactive (pointerEvents none until ready) at minHeight, with the loading skeleton overlaid at the same minHeight — no hidden->shown swap; READY grows minHeight->content (one bounded change, not 0->content). Tests (node-env .test.ts, matching the repo's vitest-include convention): - slotReservation.test.ts: empty->{false,0}, single/multi iframe-> max(minHeight)+CHROME_BAR_PX, inline-only->{true,0}, default fallback, CHROME_BAR_PX pinned at 35. - block-registry.slot-reservation.test.ts: getSlotReservation reuses listForModel and folds correctly. No schema change (read-path only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (build node egress recovered) * chore(blocks): retrigger pr-preview * chore(blocks): retrigger pr-preview (build-image flake; typecheck was green) * feat(blocks): real server-side workflow cancel (blocks.cancelWorkflow + host handler) Adds a true orchestrator-side cancel for app blocks, mirroring pollWorkflow: - blocks.cancelWorkflow tRPC procedure — verifies the block JWT + ai:write:budgeted scope, cancels on the orchestrator with the VIEWER's token (so ownership is enforced orchestrator-side, 403/404 for non-owned workflows — same gate as poll), then re-reads + returns the canceled snapshot. - IframeHost CANCEL_WORKFLOW postMessage handler → blocks.cancelWorkflow, echoes WORKFLOW_CANCELED on the matching requestId (or a failure snapshot). Pairs with @civitai/app-sdk 0.7.0 (CANCEL_WORKFLOW/WORKFLOW_CANCELED messages) and @civitai/blocks-react 0.5.0 (useBuzzWorkflow().cancel). The host uses string- literal messages so it's decoupled from the SDK publish — a block that doesn't send CANCEL_WORKFLOW is unaffected. +4 router tests (cancel happy-path + the three auth gates). * fix(app-blocks): non-empty workflowId sentinel for whatif estimate snapshots The block SDK's inbound validator (isValidWorkflowSnapshot) drops any workflow snapshot whose workflowId is an empty string. A whatif/estimate call returns no orchestrator workflow id, so snapshotFromWorkflow emitted workflowId: '' — which the SDK silently dropped, stranding ESTIMATE_RESULT until the 120s transport timeout. The block then fell back to a '≤ budget' cost instead of the real estimate (reported as 'wrong buzz cost'). Emit a 'whatif' sentinel so estimate replies validate. The block treats estimate results as a cost quote only and correlates the reply by requestId (not workflowId), so a constant sentinel is safe. Submit always carries a real id and is unaffected. +1 regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): failure snapshots must use a non-empty workflowId (durable estimate-cost fix) ROOT CAUSE of the recurring 'CTA buzz cost never updates after estimateWorkflow' (reported 5x): the block SDK's inbound validator (isValidWorkflowSnapshot in @civitai/blocks-react) DROPS any workflow snapshot whose workflowId is an empty string. The host's failureSnapshot() — returned on EVERY estimate/submit/poll/ cancel error — used workflowId:''. So when blocks.estimateWorkflow threw on the host, the host DID post an ESTIMATE_RESULT error reply, but the SDK silently dropped it (console.warn only) → the block's pending request never resolved → it hung to the transport's 120s timeout → estimatedCost stayed null → the CTA sat on its '≤ budget' fallback. The real error was swallowed twice (validator + suppressed host stdout logging), which is why this was undiagnosable for 5 reports. My earlier #55 'fix' patched the SUCCESS path (snapshotFromWorkflow, which already gets a real whatif id) — a no-op. The empty workflowId was in the ERROR path all along. Fix: failureSnapshot now stamps workflowId:'failed' (extracted to its own module src/components/AppBlocks/failureSnapshot.ts with the invariant documented + unit tested). Same fix applied to the inline insufficient-budget snapshot in submitWorkflow (was workflowId:'' → would hang submit instead of showing the top-up CTA). The block side (separate commit) now surfaces a delivered failed snapshot as an estimate error instead of silently nulling the cost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(app-blocks): retrigger pr-preview build (transient docker.io base-image TLS timeout on prior run, code typecheck was green) * feat(app-blocks): moderator-gate the entire feature (internal-only until GA) Phase 2 of the App Blocks graduation plan. The appBlocks feature flag (availability:['mod']) already hides the UI for non-mods, so the real gap was the API — close it with defense-in-depth so a direct tRPC/REST call leaks nothing even if a UI gate is bypassed. blocks.router.ts: - Convert the 23 management procedures from guardedProcedure (= any verified, not-muted user) to moderatorProcedure (= protectedProcedure .use(isMod)). guardedProcedure was NOT moderator — this was the crux gap. - For the publicProcedure block-token runtime procs (pollWorkflow, cancelWorkflow, estimateWorkflow, submitWorkflow, updateUserSettings) add an assertViewerIsModerator(userId) check on the token-RESOLVED viewer (not ctx.user). Factored into one shared helper to prevent drift. - For the session-authed reads: listForModel + listAvailable return empty for non-mods (graceful on user-facing pages); getShowcaseImages + getEffectiveCheckpoint throw FORBIDDEN. apps.router.ts (W4 KV storage): add the same assertViewerIsModerator on the resolved viewer inside the shared resolveStorageContext, covering get/set/delete/list/getQuota uniformly. block-token MINTING (api/v1/block-tokens): gate issuance on session.user.isModerator — the linchpin that makes the whole block-token runtime transitively mod-only. api/v1/blocks/me: re-assert resolved viewer isModerator (covers the ~15min window between a token mint and a demotion). Machine HMAC endpoints (api/internal/blocks/{git-push,build-callback, workflow-completed}) and the admin/webhook-token endpoints are deliberately left untouched — mod-gating them would break the Forgejo->Tekton->deploy chain and the orchestrator callback. UI: verified every /apps/* page + the model-page BlockSlot + all three nav links already gate on features.appBlocks (which is false for non-mods), and submit/review/my-submissions additionally gate on isModerator. No UI change was needed — they were already consistent. Tests: non-mod verified users now get FORBIDDEN from a sample of the formerly-guardedProcedure management procedures, from every block-token runtime proc, and from apps.storage; existing happy-path test contexts updated to moderator subjects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(app-blocks): retrigger pr-preview build (transient corepack/npm-registry fetch flake on prior run — pnpm download failed before any typecheck ran; code unchanged) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(app-blocks): reliable pr-preview retrigger (empty retrigger commit d86e3d3 produced no webhook; prior run failed only at corepack pnpm-registry fetch, a transient infra flake — code is unchanged in substance) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * security(app-blocks): close OAuth account-takeover + scope-grant gaps (audit A1-A5,A7) Implements the confirmed-CRITICAL + selected-HIGH fixes from the App Blocks security audit (claudedocs/app-blocks-security-audit-2026-06-02.md §6). All provider-side changes are scoped to app-block clients ONLY (deterministic `appblk-<slug>` id prefix) so the legitimate OAuth-apps feature is unaffected. Fix 1 (A1 CRITICAL + A2/A3/A4 HIGH) — app-block OauthClients made structurally non-interactive + scope-capped: - block-scope.constants: add isAppBlockOauthClientId discriminator (migration- free, matches the `appblk-` id prefix; OAuth-apps use uuidv4 ids) + deriveOauthBitmaskFromBlockScopes. - publish-request.service: created OauthClient now gets grants:[] (removes the Prisma default authorization_code/refresh_token) and allowedScopes = the manifest-derived bitmask (NOT TokenScope.Full). Same ceiling fed to the approve-time validator. Subsequent-version + P2002-retry paths re-cap the existing client (self-heals pre-fix Full+grants rows). - authorize.ts + device.ts: reject `appblk-*` client_id with invalid_client. - oauth-client.router: refuse update/delete/rotateSecret on app-block clients. Fix 3 (A5 HIGH) — apps:storage made a declared/approved scope: - block-scope.constants: add apps:storage:read/write (SKIP_OAUTH_CHECK). - block-scope.middleware: matching enforceContextBinding cases (no fail-open). - apps.router resolveStorageContext: assert the read/write scope per op before touching appsDb. Fix 4 (A7 HIGH) — cumulative Buzz-spend cap: - blocks.router submitWorkflow: per-(user, app_block, UTC-day) Redis counter checked before submit, incremented after success; rejects when cumulative + cost would exceed the daily ceiling. Tests: +new vitest coverage for every fix (constants discriminator/bitmask, storage scope gate, buzz cap, OauthClient scope cap). Also repaired two pre-existing test-infra breaks surfaced while validating (missing appStorage* exports in the global prom mock; wrong import path in a middleware test). Local tsc clean on all touched files (remaining log noise is the codebase-wide Prisma type-gen artifact the GH Actions PR Check resolves via prisma generate). No schema migration required (prefix discriminator). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * security(app-blocks): per-user scope-grant consent (A6) + M-BUZZMODAL + M-POPUPS Phase 3b of the App Blocks graduation, on top of the A1 fix (67bdf60a9). A6 (audit HIGH / design-gaps C2) — close silent cross-version scope escalation: - New app_user_scope_grants(user_id, app_block_id, version, granted_scopes[], granted_at, revoked_at) table + Prisma model. MIGRATION WRITTEN, NOT APPLIED (hand-apply — gotcha #14): prisma/migrations/20260602120000_a6_app_user_scope_grants. - scope-grant.service: getGrantedScopes (fail-closed on missing/revoked), recordScopeGrant (additive, un-revokes, P2002-race-safe), partitionByConsent (consent-exempt: block:settings:*, apps:storage:*). - block-tokens mint intersects requested manifest scopes with the user's grant; withholds ungranted scopes, signs only the granted subset, returns needs_consent + missingScopes to the host. Builds beneath A1's manifest ceiling. - resolveBlockInstance now resolves the pinned version's manifest/approvedScopes from app_block_publish_requests when pinned_version is set (applyPinnedVersion), with fail-safe fallback to the live row. Applied to bki_/mbi_, bus_pub_, bus_view_ branches. - Grant lifecycle: installOnModel + upsertSubscription write the implicit first-consent grant (recordInstallConsent). A scope added in a later version routes through needs_consent until re-granted. - Minimal re-consent UX: BlockConsentPrompt surfaces needs_consent above the iframe; on accept calls blocks.grantScopes (server re-caps to manifest∩approved) then refreshes the token. useBlockToken threads needsConsent/missingScopes. M-BUZZMODAL: gate OPEN_BUZZ_PURCHASE on status==='ready' (BLOCK_READY received) via resolveBuzzPurchaseRequest; no-op before ready. M-POPUPS: drop allow-popups from the unverified sandbox tier (kept for verified/internal). Tests (23 new, all green): scope-grant.service (9), block-registry.pinned-version (4), openBuzzPurchaseGate / M-BUZZMODAL (4), M-POPUPS (2), block-tokens A6 scenarios (4: granted-A-only→needs_consent for B, grant B→A+B, revoked→withheld, consent-exempt signs). No new tsc errors on touched files; no test regressions (failing block-tokens/manifest-validator cases are pre-existing on 67bdf60a9). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): update stale tests to the mod-gated / A1 / H-8 / kill_per_model_installs behavior Greens the App Blocks vitest suite after Phase 2 mod-gating + audit fixes. - block-tokens/index.test.ts: token minting now requires isModerator (Phase 2, internal-only). Add isModerator:true to the success-path sessions; add an explicit non-mod-rejected-at-mint-gate test; make the ban/soft-delete cases mods so they exercise their own gate (not the upstream mod-gate). - block-manifest-validator.service.test.ts: H-8 added the allowedOrigins ceiling on iframe.src. Pass an AppContext whose allowedOrigins covers the manifest src for the success-path cases (the bare-number form defaults to [] and rejects). - checkpoint.service.test.ts: publisher install settings now resolve through BlockRegistry.resolveBlockInstance (model_block_installs was absorbed into block_user_subscriptions by kill_per_model_installs). Mock the resolver instead of the retired dbRead.modelBlockInstall.findUnique seam. - block-token.service.test.ts: provision the RSA keypair in test setup (the service reads env/server's import-time snapshot, so a beforeAll process.env set was too late) and verify with a KeyObject (jose v6 rejects a PEM Buffer). - setup.ts: wire a real BLOCK_TOKEN_{PRIVATE,PUBLIC}_KEY pair into the mocked env defaults; re-export the public PEM for the round-trip test. - prisma/models.ts: regenerated to include the App Blocks model interfaces the branch schema already defines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): fix module-load crash in blocks.router.subscriptions test The suite crashed at import time (0 tests ran) because blocks.router imports getUserBuzzAccounts from buzz.service, which transitively loads redis/caches -> orchestrator/models -> resource-data.redis. That last module reads REDIS_KEYS.GENERATION.RESOURCE_DATA at module scope, which threw under the test's trimmed redis-client mock (no GENERATION key). Mock buzz.service at the boundary to cut the chain -- the same approach the sibling blocks.router.workflow.test.ts already uses. Also realign the two upsertSubscription settings-validation tests to the current manifest-driven validator (W3 validateBlockSettings) instead of the removed hardcoded per-blockId schema: the mocked appBlock now declares the buzz_budget_per_gen field on manifest.settings (publisher- and viewer-scoped respectively) so the range checks actually exercise the live code path. All 22 tests now run and pass, including the 8 Phase 2 non-mod -> FORBIDDEN security assertions that previously provided zero coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): harden cheap MEDIUM/LOW audit findings (M-WEBHOOK, L-CALLBACK, L-SANDBOX, L-DEDUP, L-M2, L-VERIFY/L-M6) App Blocks security audit (2026-06-02) §4 follow-up. Each fix is the smallest-correct change plus a vitest test. - M-WEBHOOK (git-push.ts): verify the push repo's org. The shared FORGEJO_WEBHOOK_SECRET authenticates the Forgejo *instance*, not a repo — the same instance also serves the civitai-apps-review org. Derive the slug from repository.full_name and require the canonical civitai-apps org (parseExpectedRepo), instead of trusting repository.name alone. - L-CALLBACK (build-callback.ts): bind the accepted imageRef to the callback's own slug + sha (expectedImageRef = ghcr.io/civitai/app-block- <slug>:<sha>). The bare app-block- prefix check let a signature-valid callback for slug A deploy app-block-<B>:<sha>, and accepted mutable :latest. - L-SANDBOX (sandbox.ts, extracted from IframeHost): intersectSandbox now fails closed to an explicit minimal safe set (allow-scripts) and unions declared+minimal, so it can never be wider than what the manifest declared; allow-same-origin stays tier-gated. - L-DEDUP (usePostMessage.ts): read the dedup requestId from payload (where the SDK transport puts it) via extractRequestId, not the always-undefined top-level data.requestId — replay dedup was inert. - L-M2 (attribution.schema.ts + attribution-validator.service.ts): align the attribution scope vocab post-kill_per_model_installs. mbi_*/bki_* are now per-model-PINNED publisher subscriptions, so both deriveScopeFromInstanceId and SOURCE_TO_SCOPE.install map them to publisher_all_my_models (same V2 publisher rate — no payout change, one bucket). per_model_install kept in the enum/rate-card for historical rows. - L-VERIFY / L-M6 (block-scope.middleware.ts): fail closed. verifyBlockToken now requires a kid and verifies against exactly that key (no fan-out to all keys); isBlockJwt requires typ=JWT exactly; the enforceContextBinding switch gets a default-deny. Verified safe: BlockTokenService.sign has stamped kid + typ:JWT on every token since the first App Blocks commit (5bf6f05b6), tokens live 15m and re-mint each render — no kid-less issuance era exists. Net test delta: 0 new failures (the one storage-provision.service.test.ts failure is pre-existing on the clean tip). Touched files are tsc-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): adapt to React Query v5 API after main merge main's merge brought a React Query v4->v5 upgrade that broke the App Blocks UI code the Type Check caught (12 errors): - query option `keepPreviousData: true` removed -> `placeholderData: keepPreviousData` (import from @tanstack/react-query) in useBlockSlot.ts - mutation result `.isLoading` renamed to `.isPending` (AppSettingsModal, PublisherSubscriptionBanner, my-submissions, review, submit). Query `.isLoading` left as-is (still valid in v5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): A8/BUILD-1 Phase 2 — drop tenant Dockerfile/nginx from build-source commit The build pipeline injects its own platform-owned Dockerfile + nginx.conf and ignores any tenant-supplied copies (gpu-fleet-infra #21). Committing the tenant copies to the canonical Forgejo repo (civitai-apps/<slug>, which the build clones) is therefore inert + misleading. Filter platform-owned paths (Dockerfile, nginx.conf, case-insensitive, repo-root) out of the approve commit. The in-review snapshot (civitai-apps-review) + the diff summary keep the full upload so mods still see exactly what the dev sent. Updated the orchestration test's commit assertion to reflect the dropped Dockerfile. Remaining Phase 2 (follow-ups): submit-time reject/warn on tenant build files; update the starter template (Forgejo civitai-apps/starter) + blocks-cli scaffold + SDK docs to not emit a Dockerfile. See datapacket-talos/claudedocs/app-blocks-a8-phase2-civitai-web-followup-2026-06-03.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): anonymous conversion — REQUEST_SIGN_IN + anon-safe token mint App Blocks "anonymous conversion": a logged-out viewer sees the full block rendered (from the scope-free BLOCK_INIT context); clicking an action that needs auth/money (Generate) prompts sign-in instead of erroring. Token mint (src/pages/api/v1/block-tokens/index.ts): - Replace the hardcoded `isModerator` mint gate with a feature-availability check on the `appBlocks` flag evaluated for the (possibly-null) session. Prod flag is `availability: ['mod']`, so behaviour is UNCHANGED (only mods mint); when the flag goes public, anon/non-mod can mint. Rate-limits + banned/deleted checks unchanged. - Anon (userId == null): instead of 403-ing on `:self` scopes, issue the anon-safe subset = manifest scopes with every consent-gated scope STRIPPED (the COMPLEMENT of CONSENT_EXEMPT, via consentGatedScopes). This withholds every `:self`/owned/money/tip scope (ai:write:budgeted, buzz:read:self, user:read:self, social:tip:self, media:read:owned, models:read:self). For generate-from-model the anon subset is empty → Generate stays server-gated. Fail-closed: any future money/self scope is stripped for anon by default. Host (src/components/AppBlocks/IframeHost.tsx + requestSignInGate.ts): - New inbound REQUEST_SIGN_IN handler (payload {returnUrl?}). Pinned by usePostMessage (origin + event.source) and gated on status==='ready' (post-BLOCK_READY) via the pure resolveRequestSignIn gate; triggers the civitai LoginModal (reason 'image-gen'). returnUrl is open-redirect-guarded (same-origin in-app path only), defaulting to the current page otherwise. Tests (vitest): anon mint strips money/self scopes (not 403) when appBlocks is available; anon mint rejected when appBlocks NOT available; authed-mod mint unchanged; REQUEST_SIGN_IN honored only after BLOCK_READY + returnUrl sanitised. Prod `appBlocks: ['mod']` flag left AS-IS (not flipped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(app-blocks): collapse failed blocks to null instead of a broken card A block that fails to load (timeout / fatal / no_token / token_error / bad manifest src) showed a visible BlockFallback card. Change every terminal- failure path to render null so the slot collapses and takes no space — a failed block shows nothing. - IframeHost: terminal-failure branches (malformed src, 'timeout', 'fatal', 'no_token') now return null. Decision extracted to the pure, unit-tested hostRenderDecision helper (node-env testable; mirrors the W7/W8 sortInstallsForSlot / failureSnapshot pure-helper pattern). - BlockHost: the token-mint error path (the 'authorization error' card) returns null too — that's the primary token_error fallback. - Preserve the W7 trust chrome on the READY state and the brief loading skeleton during 'loading'. Rendering null on failure shows no content, so the FRAME-1 anti-spoofing property is not weakened (nothing to masquerade as); no reserved min-height gap on failure (the slot's loading reservation is a transient pre-data state, gone once installs resolve). Tests: hostRenderDecision asserts each terminal-failure status collapses and ready/loading render content (60 AppBlocks helper tests green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(app-blocks): gate listForModel/getShowcaseImages by appBlocks flag (not isModerator) + null-safe showcase reactionCount Two anon-conversion bugs — the feature relaxed the appBlocks flag to public + the block-token mint, but two server reads still hard-gated on isModerator, so anon viewers' blocks rendered (flag public) yet never received data: 1. blocks.listForModel returned [] for any non-moderator BEFORE calling BlockRegistry.listForModel — anon never got installs (blocks invisible). Now gates on ctx.features.appBlocks (mirrors the client useFeatureFlags() gate + the mint gate); viewerUserId tolerates anon (ctx.user?.id ?? null). Prod-safe: appBlocks is ['mod'] in prod, so non-mods still get [] pre-GA. 2. blocks.getShowcaseImages threw FORBIDDEN for non-mods AND 500'd on reactionCount: (a) same flag-gate fix (returns [] when flag off, so anon blocks can load showcase once public); (b) ImageMetric.reactionCount is declared non-nullable Int in schema.prisma (no @default) but is NULL in prod, so the typed select threw 'Error converting field reactionCount ... found null' (P2032). Fetch counts via null-tolerant $queryRaw + ?? 0 (the author's original intent) instead of the typed metrics relation. Root-caused via Loki structured logging on the pr-2447 instrumented build. * fix(app-blocks): gate getEffectiveCheckpoint by appBlocks flag too (anon checkpoint resolution) Third instance of the same Phase-2 moderator-gate the anon-conversion feature missed: blocks.getEffectiveCheckpoint threw FORBIDDEN for non-mods, so an anon viewer's block errored on checkpoint resolution (console error on the rendered block) even with the flag public. Now gates on ctx.features.appBlocks (returns {checkpoint:null} → block falls back to the platform per-ecosystem default); getEffectiveCheckpoint already accepts userId: number|null so anon is null-safe. Completes the anon read path alongside listForModel + getShowcaseImages. * fix(app-blocks): un-gate grantScopes (consent) from moderator → protected + flag Fourth Phase-2 mod-gate the anon-conversion missed: blocks.grantScopes was a moderatorProcedure, so a logged-in NON-MOD viewer could never grant the consent-gated scopes their block needs (ai:write:budgeted etc.) — meaning the A6 consent flow could surface 'needs_consent' but the viewer had no way to actually consent, and the block could never spend their buzz. Now protectedProcedure (authenticated) + ctx.features.appBlocks gate. Grant stays bounded to the app's approved manifest ∩ approvedScopes ceiling and writes only the caller's own app_user_scope_grants row. * fix(app-blocks): move pages/api __tests__ out of src/pages for Next 16 build The merge brought main's Next 14 -> 16 upgrade. Next 16's `next build` type-checks every file under src/pages/** as a route, so the 4 App-Blocks handler tests under src/pages/api/**/__tests__/ failed ("does not satisfy ApiRouteConfig") — gotcha #45. Moved them to src/tests/api/** (the existing convention, e.g. src/tests/api/v1/images) and switched the two relative handler imports to ~/pages/... absolute paths. No behavior change; restores a green Next-16 build. * fix(app-blocks): repoint moved test imports to ~/pages absolute paths Follow-up to the test-file move (d05e6ac0c): block-tokens/index.test.ts (25 dynamic import()/vi.mock refs) + developer/block-manifests.test.ts (3) + the two internal/blocks tests still referenced their handlers via relative '../' paths, which broke once moved out of src/pages → TS2307 on typecheck. Repointed all to ~/pages/api/... absolute aliases. * feat(app-blocks): make models:read:self consent-exempt (allow-by-default) Step 1 of the lazy-consent UX: models:read:self is a low-sensitivity read of the viewer's own models (no-op for anon → safe in an anon token), so it no longer requires a per-user grant. The block can render fully for a logged-in viewer with no upfront consent prompt; the consent gate is now reserved for the money/ AI scopes (ai:write:budgeted, buzz:read:self). Step 2 (request those lazily on the Generate click instead of on load) is the block + IframeHost follow-up. * feat(app-blocks): lazy consent — request scopes on the action, not on load The block now renders in full for a logged-in viewer who hasn't granted every consent-gated scope; consent is requested when they click an action that needs it (e.g. Generate), not via an at-load Alert. - IframeHost: trim the wrapped token's `scopes` to what the mint actually signed (manifest scopes minus `missingScopes`) so the block's capability check is accurate; gate buzzBudget on the granted scopes; add a REQUEST_CONSENT handler that opens BlockConsentModal for the server-known missing set, grants via blocks.grantScopes, and re-mints (TOKEN_REFRESH carries the new scopes → the block retries). Gated on status==='ready' via the pure resolveRequestConsent helper. - BlockHost: drop the at-load BlockConsentPrompt; pass missingScopes + onConsentGranted(refresh) to IframeHost. Removes the now-dead BlockConsentPrompt component. - BlockConsentModal: point-of-action consent modal (replaces the Alert). - Fix two stale block-tokens mint tests that predated 01ea90441 making models:read:self consent-exempt (anon + revoked-grant now keep it). 89 AppBlocks/block-tokens tests pass; tsc adds no new errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): "Hide app block" — viewer-local dismiss of owner-installed blocks A model owner's "show on my models" block renders to every viewer. Add a "Hide app block" item to the host trust-frame's ⋯ menu so a viewer can locally dismiss one without affecting the publisher's install or anyone else. - hiddenBlocks.ts: localStorage-backed (per blockInstanceId), SSR-safe, reactive via an in-page event + cross-tab `storage`. hideBlock() + isBlockHidden() + useHiddenBlocks(). - IframeHost/AppBlockChrome: the new menu item calls hideBlock(instanceId). - BlockSlotClient: filters hidden installs out of the render list, so a hidden block unmounts immediately AND never mounts (no token mint) on reload; an all-hidden slot collapses to nothing. Note: no unhide UI yet — recoverable only by clearing localStorage. + hiddenBlocks unit tests (happy-dom). Typecheck + AppBlocks tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): "Hidden" tab on /apps/installed to restore hidden blocks Pairs with the ⋯-menu "Hide app block": gives viewers a way back. The hidden store now keeps a little metadata per instance (app + model name, hidden-at) so the restore list reads meaningfully with no server lookup; back-compat reader migrates the original string[] shape. - hiddenBlocks.ts: record shape + unhideBlock() + useHiddenBlockList(); hideBlock() now takes a HiddenBlock (instanceId + app/model labels). - IframeHost/AppBlockChrome: pass app + model context into hideBlock. - /apps/installed: new "Hidden" tab listing hidden blocks with a Restore button (reactive — restoring re-shows the block on its model page). + migration + unhide tests. Typecheck + AppBlocks/Apps suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: add registerInstrumentationMetric/Histogram/GaugeWithLabels to prom mock Merge follow-up: main's eventloop-longtask.ts registers a histogram + counter via registerInstrumentationMetric AT MODULE LOAD, and trpc.ts imports it — so every router test (e.g. blocks.router.*) loads it. The global prom/client mock in setup.ts predated those exports, so the tests failed at import with `No "registerInstrumentationMetric" export defined on the mock`. Add the three metric-factory helpers (additive). * fix(app-blocks): un-gate the /apps/installed own-data procs (moderator→protected) The manage-page queries listMySubscriptions / listMyScopeGrants / listMyAppActivity / listMyScopeInvocations + the own-data management actions uninstallFromModel / setSubscriptionPinnedVersion were moderatorProcedure (the internal-only "remove/relax at GA" gate). But /apps/installed gates per-user on features.appBlocks, so on flag-public surfaces (preview/GA) the page admits non-mods while every tab's query threw FORBIDDEN. Relax to protectedProcedure + the existing enforceAppBlocksFlag middleware (gotcha #66 pattern). Prod-safe: the page's per-user flag still blocks non-mods there; each proc is self-scoped — the reads to ctx.user.id, uninstallFromModel via assertCanManageBlocks (model-owner-or-mod), setSubscriptionPinnedVersion via the service's 'not the subscription owner' guard. Install/upsert/ delete + the mod-review queue + revenue/apps stay mod-gated. + flip the stale listMySubscriptions→FORBIDDEN test to a non-mod success assertion. 99 blocks.router/user-app-surface tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): fix two stale pre-existing test files (scope-grant, showcase) Pre-existing failures on feat (unrelated to the merge), mopped up: - scope-grant.service.test: 01ea90441 made models:read:self consent-exempt, but partitionByConsent / consentGatedScopes still asserted the old exempt set. models:read:self now signs without a grant + is dropped from the gated set. (Same staleness already fixed in block-tokens/index.test.) - showcase.service.test: the reactionCount-is-NULL P2032 fix moved reaction counts to a raw $queryRaw using Prisma.join — which the test never mocked (Prisma.join undefined in the test env). Mock Prisma.join + derive the AllTime ImageMetric rows from the findMany fixture in $queryRaw, so the existing imageRow(...) call sites are unchanged. 432 App-Blocks router/service/component tests now green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): render the model.sidebar_top block below the carousel on mobile On small screens the sidebar grid column stacks full-width, and a mobile-only ModelCarousel renders inside it. The BlockSlot sat ABOVE that carousel, so the app block pushed the image carousel down the page. Move the BlockSlot to just after the mobile carousel: on small screens the block now sits BELOW the carousel; on sm+ the mobile carousel renders nothing, so the block keeps its sidebar-top position (the gallery is in the other grid column). Pure reorder — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): fix faulty storage-provision rollback assertion + add merge audit The 'rolls back when a statement throws' test matched the throwing DDL with sql.startsWith('CREATE TABLE …'), but the service emits that DDL as an indented template literal (leading whitespace), so the mock never threw and provision() resolved instead of rejecting. Fix the matcher to trimStart().startsWith(...). The production rollback path (COMMIT in try / catch ROLLBACK+throw / finally release) was already correct — this was a test-only bug now caught by main's full-vitest CI gate (#2489). Also add docs/features/app-blocks-merge-audit-2026-06.md capturing the pre-merge audit (gating/H2 flag-divergence, security, DB/migrations, money paths). * refactor(app-blocks): isolate 72mb body limit to a dedicated upload route The W1 publish-request bundle (base64 ZIP, ~67 MiB encoded) was the only payload exceeding the shared tRPC body limit, and accommodating it had lifted /api/trpc/[trpc] to 72mb for EVERY tRPC call app-wide. Move the upload to a dedicated POST /api/blocks/submit-version route: - 72mb body limit isolated to this one endpoint - ModEndpoint (moderator session) + appBlocks-flag gate + bundle-storage check — auth/behaviour parity with the former blocks.submitVersion tRPC mutation - delegates to the unchanged submitVersion service Revert /api/trpc/[trpc] to 17mb; remove the now-dead blocks.submitVersion tRPC procedure + its unused schema import; rewire /apps/submit to POST the route via a react-query useMutation (same .mutate/.isPending semantics). Verified submitVersion was the only >17mb tRPC path (KV storage.set capped at 64KB). Service tests unchanged (72 pass). * test(app-blocks): handler coverage for POST /api/blocks/submit-version Covers the route's auth/flag/validation shell (the only new logic from the body-limit isolation): ModEndpoint moderator gate (405 non-POST, 401 no-session /non-mod/banned), appBlocks flag (503), bundle-storage precondition (412), schema validation (400 empty/missing), success (200 — service called with the decoded buffer + moderator id), and service-error mapping (400 w/ message). Drives the real ModEndpoint so the gate is genuinely exercised; mocks only auth/env/flag/infra + the (separately-tested) submitVersion service. * test(app-blocks): fix submit-version test to not drive the real withAxiom The first version mocked @civitai/next-axiom, but ModEndpoint's withAxiom closure is captured at endpoint-helpers module-load — in the full-suite run (shared module registry) that load can happen via another file before the per-file mock applies, so the REAL withAxiom ran and hit res.once (passed in isolation, failed in CI). Follow the repo's retool-endpoint.test.ts convention: mock ~/server/utils/endpoint-helpers and provide a ModEndpoint stub that reproduces the real gate verbatim (method→405; session+isModerator+!banned→401). The flag/storage/validation/decode/service branches still run the real handler body. Verified across a 28-file/394-test multi-file run. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: zach <zach@civitai.com>
2026-06-13 10:20:57 -05:00
"yaml": "^2.8.1",
"zod": "^4.0.17",
2024-04-24 16:42:52 -06:00
"zustand": "^4.3.7"
2022-10-11 16:56:51 -04:00
},
"devDependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.2.6",
feat: App Blocks v1 — block-host substrate, CORS, JWT, publisher-install (model.sidebar_top slot) (#2319) * feat(blocks): App Blocks v1 — substrate, JWT, manifest registry, model.sidebar_top Implements App Blocks v1: a substrate for rendering third-party iframe-embedded blocks on civitai model pages, authenticated via short-lived RS256 JWTs scoped to individual block installs. Architecture: docs/features/app-blocks.md (new). DATABASE (prisma/migrations/20260524120000_app_blocks_initial): - app_blocks: registry; status, trust_tier, render_mode, approved_scopes[], v2 substrate columns (asset_bundle_*) - model_block_installs: per-(model, slot) install rows; composite UNIQUE (model_id, app_block_id, slot_id); installed_by SET NULL on user delete; FK indexes for delete pipelines; slot_id CHECK; TEXT PK length CHECKs - block_user_settings: per-(viewer, instance); CASCADE on install + GDPR user delete - platform_default_blocks: moderator-promoted defaults; partial index (slot_id, priority) WHERE enabled; SET NULL on promoter delete tRPC blocks router (src/server/routers/blocks.router.ts): - listForModel: public, slot enum-validated, flag-gated [] when off, threads modelType + modelNsfwLevel for content-rating filter - installOnModel / updateSettings / toggleEnabled / uninstallFromModel: guardedProcedure (verified + non-muted); dbWrite for auth lookups; updateSettings pins modelId in WHERE; install cap enforced at insert (rejects 4th); byte-length 4KB cap withBlockScope middleware (src/server/middleware/block-scope.middleware.ts): - RS256-only JWT verify; kid-based key select with NEXT-rotation fallback; clockTolerance: 30s, maxTokenAge: 15m; scalar assertions on iat/exp/jti/aud - Per-scope context binding: models:read:self → query.id integer-match; media/buzz/social/user:read:self → non-anon sub; ai:write:budgeted → positive buzzBudget; block:settings:* → blockInstanceId match (decimal-only modelId parse; array-form query rejected) - Deny-by-default for unknown scopes - Per-instance revocation check (Redis marker) - CORS/cache isolation: wraps res.setHeader to prevent wrapped PublicEndpoint/AuthedEndpoint from clobbering; forces Cache-Control: private, no-store on block-JWT responses API endpoints: - POST /api/v1/block-tokens: same-origin EXACT host match (rejects POST without Origin + non-allowlisted Origin with 403); per-IP rate limit with in-process LRU fallback; CF-Connecting-IP only when cf-ray present; per-(user/ip, instance) rate limit BEFORE DB lookup; ban/mute/deleted gate at issuance; OAuth-bit scope allowlist + approved_scopes snapshot intersection; settings tokens require caller==installer + 5-min TTL; client slotContext allowlist + scalar coerce; server stamps modelId + slotId; Flipt-gated 503 - GET /api/v1/block-tokens/jwks: 60s cache + ETag; 503 when not configured or malformed; flag-gated - GET /api/v1/blocks/me: user:read:self; banned rejected; dbWrite - POST /api/v1/developer/block-manifests: JOB_TOKEN timingSafeEqual; 64KB bodyParser cap + byte-length 32KB manifest cap; trustTier/renderMode FORCED to unverified/iframe on INSERT (admin promotion is a separate Phase 2 path); UPDATE resets status='pending' + 403s on tier change; byte-equal no-op short-circuit; flag-gated - POST /api/internal/blocks/workflow-completed: JOB_TOKEN; Redis- backed workflowId idempotency (7-day TTL, fail-closed); flag-gated V1 route wrapping: - /api/v1/models/[id] wrapped with withBlockScope (models:read:self) - /api/v1/me unchanged; App Blocks use the dedicated /api/v1/blocks/me Manifest validator (src/server/services/block-manifest-validator.service.ts): - Trust-tier-gated sandbox token allowlist - SSRF gate: rejects RFC1918, loopback, link-local, IPv6 ULA fc00::/7, zone identifiers, .internal/.local/metadata.*, dotted + dotless hex/octal/integer IPv4, IPv4-mapped IPv6 - Manifest URLs bound to OauthClient.allowedOrigins (H8) - Iframe height envelope; sandbox non-empty; publicSettingsKeys allowlist for listForModel exposure React tree (src/components/AppBlocks/): - BlockSlot: Flipt-gated mount; keyed on (slotId, modelId) so navigation force-unmounts; renders nothing when no installs - BlockHost + BlockErrorBoundary: error containment - IframeHost: full BLOCK_INIT → BLOCK_READY → RESIZE_IFRAME lifecycle; iframe-loaded as state so 10s timeout arms after token-late-load; 15s token-wait timeout; empty-src early fail; hard 8000px height ceiling + isFinite guards; referrerPolicy=no-referrer, loading=lazy; client-side sandbox intersection; RESIZE gated on ready; TOKEN_REFRESH postMessage on token rotation (no remount) - useBlockToken: AbortController per request; absolute refreshAtRef drives visibility-resume; document-hidden pause; jittered 429 backoff; doesn't set pending=true on refresh - usePostMessage: origin match + event.source window-identity pin; rate limit + LRU dedup Feature flag (Flipt key app-blocks-enabled): Gates every server-side surface AND the BlockSlot mount. Off by default; flag is the launch lever + kill switch. Tests (vitest): - block-scope constants - manifest validator (sandbox + SSRF + binding + size + content rating) - context binding + JWT classics (alg=none, HS256 confusion, expired, wrong iss/aud) - block-registry SQL invariants + install cap + content rating + publisher settings projection + toggleEnabled revocation cycle - block-token handler (CSRF reject, flag-off, banned/deleted, settings ownership, scope allowlist, ctx coercion) - manifest registration (status reset + trust-tier lockdown) Pre-launch checklist (deploy-side): - BLOCK_TOKEN_PRIVATE_KEY + BLOCK_TOKEN_PUBLIC_KEY set - BLOCK_ALLOWED_ORIGINS includes prod blocks origin - blocks.civitai.com without X-Frame-Options: DENY - Flipt app-blocks-enabled flag toggled on - CF-only ingress on civitai-main (per-IP rate limit depends on it) Phase 2/3 deferred (documented in PR description): publisher install UX, admin tier-change tool, moderator approval UI, audit log table, ClickHouse telemetry, health-check + auto-suspend, per-jti revocation denylist, per-app OAuth (replacing JOB_TOKEN), DNS-rebinding gate at fetch time, CSP frame-src on model pages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(app-blocks): unblock iframe load + give trusted blocks real origin Two issues prevented BlockHost from ever showing the iframe in PR-2319: 1. loading="lazy" + initial display:none deadlocked the load. With the iframe out of layout it's never "near viewport", so the lazy gate never fires, onLoad never runs, status never transitions to ready, and display stays none. Drop loading="lazy" so the iframe loads on mount; size + visibility are still controlled via inline style. 2. The client-side sandbox allowlist strips allow-same-origin unconditionally, but the rest of the messaging design assumes a real iframe origin: usePostMessage.send uses an explicit targetOrigin = new URL(iframe.src).origin and usePostMessage's inbound listener gates on event.origin === expectedOrigin. An opaque-origin iframe ("null") never matches either side, and its subresources also fail CORS at the static host. Permit allow-same-origin for trusted tiers (internal, verified) so this works as designed. Unverified blocks still get an opaque origin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(blocks): export verifyBlockToken for tRPC reuse Workflow procedures in blocks.router need the same JWT verification gate as the Next.js API middleware. Exporting the existing helper keeps signer/verifier behavior in one place. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): wire workflow procedures (poll/estimate/submit) Adds the host↔orchestrator bridge the App Blocks SDK expects. Blocks can now drive useBuzzWorkflow().{estimate,submit,poll} end-to-end: - pollWorkflow: read status via the user's orchestrator token (orchestrator enforces ownership server-side) - estimateWorkflow: cost preview via submit + whatif=true; no budget gate - submitWorkflow: budget gate via cost preflight, anon rejection, prompt audit before any orchestrator call. Over-budget returns a failed-shape snapshot rather than throwing — the SDK treats throws as block lifecycle errors but expects budget rejections as recoverable outcomes. All three verify the block JWT via the shared verifyBlockToken helper and re-check context binding (claims.ctx.modelId === input.body.modelId) plus the modelVersionId → modelId DB chain. Workflow body schema is a strict discriminated union (textToImage only for v1). Server fills baseModel from the version row and conservative defaults (sampler=Euler, steps=25, dimensions per base-model family) so blocks don't need to know platform-side gen params. Tags every block-submitted workflow with app-block:{appId,block,instance} for billing attribution and post-incident review. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): host workflow + buzz-purchase postMessage bridge Subscribes IframeHost to the four block→host messages the SDK now expects: - SUBMIT_WORKFLOW → trpc.blocks.submitWorkflow → WORKFLOW_SUBMITTED - ESTIMATE_WORKFLOW → trpc.blocks.estimateWorkflow → ESTIMATE_RESULT - POLL_WORKFLOW → trpc.blocks.pollWorkflow → WORKFLOW_STATUS - OPEN_BUZZ_PURCHASE → BuyBuzzModal → BUZZ_PURCHASE_RESULT Every handler validates requestId is a string (drop otherwise) and echoes it back verbatim — the SDK's sendTypedRequest correlates by id and times out after 30s if we never reply. tRPC errors are converted to failure-shape snapshots rather than thrown, so the block can render "top up Buzz" CTAs instead of seeing a lifecycle error. OPEN_BUZZ_PURCHASE caps the attacker-controlled `suggestedAmount` at 50k buzz before pre-filling the modal, and uses a per-requestId dialog id so overlapping requests don't collapse in the dialog store dedup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(blocks): cover workflow service helpers + router gates 32 tests across two files: - workflow.service.test.ts: snapshotFromWorkflow status mapping, image-url filtering (drops pending/empty/blocked), version resolver's not-found vs forbidden gates, buildTextToImageInput defaults per base-model family. - blocks.router.workflow.test.ts: every security gate on each procedure (invalid token, missing scope, modelId mismatch, version belongs to different model, anon submit, over-budget, prompt-audit fails closed, flag disabled, malformed body). Asserts the cost preflight + real submit are wired in the right order and that whatif is set on the estimate path. IframeHost handler tests are deferred — this repo has no React component test infrastructure (vitest runs in node env, no jsdom or testing-library setup). Adding that is its own task. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): drop tier from User select — not a Prisma column getBlockSessionUser was selecting `tier: true` from the User table, which Prisma rejects: tier isn't on User. It's derived from active subscriptions and stamped on SessionUser at session-creation time (see types/next-auth.d.ts). Block-initiated calls fall through to free-tier limits via the `user?.tier ?? 'free'` default the orchestrator helpers already apply. Higher-tier users get free-tier limits when generating through a block — acceptable for v1; if blocks need parity with web generation we'll mirror the session tier-resolution logic in a follow-up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): prepend platform checkpoint for non-Checkpoint models The orchestrator rejects workflows with no Checkpoint in resources — the run needs an anchor model. Blocks bound to a LoRA were sending just `[{ id: loraVersionId }]`, hitting "A checkpoint is required to make a generation request" at parseGenerateImageInput. When the bound model isn't itself a Checkpoint, prepend the platform's per-family default. v1 wires Flux1 only (version 691639, the fluxStandardAir canonical checkpoint). Other base-model families return BAD_REQUEST with a clear message until product picks canonical checkpoints for them — that's a buzz-attribution + UX decision, not something we should hardcode silently. resolveBlockVersionContext already returns modelType; widening the buildTextToImageInput signature is the only change at the call sites. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): real checkpoint-selector chain (deletes band-aid map) Replaces the hardcoded DEFAULT_CHECKPOINT_VERSION_BY_FAMILY={Flux1: 691639} map with a real per-install / per-viewer selector. Server-side only — the SDK + block-app changes ride in a follow-up. Data lives in two existing JSONB columns; no migration: - model_block_installs.settings.default_checkpoint_version_id (publisher) - block_user_settings.settings.checkpoint_version_id (viewer) Precedence chain at submit time (checkpoint.service.ts): 1. Bound model is a Checkpoint → it's its own anchor (skip overrides) 2. Viewer override (re-validated; drop-on-invalid → fall through) 3. Publisher default (re-validated; throws on invalid so author sees it) 4. BAD_REQUEST — no platform fallback. Install is misconfigured. Validation distinguishes not-found / not-published / not-a-checkpoint / wrong-ecosystem via TRPCError.cause.reason so the install-form UI can render inline errors. New surface: - src/server/schema/blocks/settings.schema.ts: per-block-id typed shapes - src/server/services/blocks/checkpoint.service.ts: validateBlockCheckpoint, getRepresentativeBaseModel, resolveBlockCheckpoint - blocks.updateUserSettings tRPC (block JWT-gated, host-mediated) - blocks.getEffectiveCheckpoint tRPC (publisher ∪ viewer merge for BLOCK_INIT) - BlockRegistry.upsertUserSettings / getUserSettings / getEffectiveCheckpoint - BlockInstallRecord.defaultCheckpoint (anon-safe — viewer override is delivered separately through the new query) - BlockRegistry.listForModel: batched ModelVersion join populates defaultCheckpoint without N+1 - buildTextToImageInput: dropped the family map, now takes an explicit checkpointVersionId from the router after resolveBlockCheckpoint Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): host picker handlers + context.checkpoint merge Three IframeHost changes: 1. BLOCK_INIT.context.checkpoint — IframeHost now fetches the effective (publisher-default ∪ viewer-override) checkpoint via the new blocks.getEffectiveCheckpoint query and merges it into the init payload BEFORE sending. Init waits for the query to land so the block never sees a stale value and re-mount. 2. OPEN_CHECKPOINT_PICKER → opens the platform's existing openResourceSelectModal filtered to Checkpoints in the requested ecosystem (baseModelGroup expanded via getBaseModelsByGroup). Posts CHECKPOINT_PICKER_RESULT with the selection, or an empty result on dismiss. Guards against double-emission via an `answered` latch since the modal calls onSelect THEN onClose on successful pick. 3. SET_USER_CHECKPOINT → calls trpc.blocks.updateUserSettings with the block token, refetches getEffectiveCheckpoint so a subsequent BLOCK_INIT reflects the new value, posts USER_CHECKPOINT_SET with ok/error shape. ModelSlotContext type extended with the optional `checkpoint` field (BlockCheckpointInfo). null when no checkpoint configured AND model isn't itself one — block renders a "missing checkpoint" state in that case. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): debug endpoint for setting install defaults Standalone WEBHOOK_TOKEN-gated endpoint at /api/testing/blocks for setting the publisher default checkpoint and buzz budget on a block install. Until a publisher-facing install UI ships (separate UX initiative), this is how the demo install (mbi_01KSD3NP23EQHXEPQRH32EX72G) gets configured. Routes through BlockRegistry.updateSettings so the same per-block-id validation runs (ecosystem match, Published status, Checkpoint type). Actions: set-default-checkpoint, set-buzz-budget, show. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(blocks): checkpoint service + router precedence chain 19 new tests covering the checkpoint resolution chain end to end. checkpoint.service.test.ts (15 tests): - validateBlockCheckpoint: every failure mode with distinguishable cause.reason (not-found / not-published / not-a-checkpoint / wrong-ecosystem); same-family-different-baseModel match (Flux.1 D ↔ Flux.1 S). - getRepresentativeBaseModel: published → unpublished fallback → null. - resolveBlockCheckpoint: Checkpoint-self short-circuit (no DB reads), viewer override beats publisher default, stale viewer override drops through to publisher default, no override + no default = BAD_REQUEST, publisher-default validation failures surface (not silenced). blocks.router.workflow.test.ts (4 new tests in LoRA-install describe): - BAD_REQUEST when no publisher default AND no override - publisher default used when override missing - viewer override beats publisher default - stale override falls through cleanly The original 16 router tests still pass — they use Checkpoint-type fixtures that short-circuit through resolveBlockCheckpoint's self-anchor branch, so the new precedence chain doesn't regress them. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): platform per-ecosystem checkpoint fallback + fix picker filter Two changes that make LoRA installs Just Work without per-install configuration. 1. Picker filter normalization The block sends effectiveCheckpoint.baseModel (e.g. "Flux.1 D") to OPEN_CHECKPOINT_PICKER, but getBaseModelsByGroup expects an ecosystem key (e.g. "Flux1"). Result: empty filter → no checkpoints visible in the picker. Wrap with getBaseModelGroup, which accepts both forms and normalizes to the ecosystem key. 2. Platform per-ecosystem fallback New rung in the precedence chain: when no publisher default AND no viewer override, pick the most-thumbed Published Checkpoint with at least one version in the LoRA's ecosystem family. Cached in Redis 1h. Used by both resolveBlockCheckpoint (submit-time) and BlockRegistry.getEffectiveCheckpoint (BLOCK_INIT-time) so the iframe and the orchestrator agree on the same anchor. BAD_REQUEST is now only thrown when the ecosystem has zero Published Checkpoints — a real edge case (brand-new base model with only LoRAs). The "ask the model owner" message is gone for normal installs; the demo works out of the box on any ecosystem with a popular Checkpoint. Adds REDIS_KEYS.BLOCKS.POPULAR_CHECKPOINT for the 1h cache; outage fails open to the DB query. 22 new test cases (4 new in router; 5 new + 1 updated in service). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): pivot popular-checkpoint query through ModelMetric CI Type Check failed: Prisma can't `orderBy` a scalar through a 1:many relation (Model.metrics is declared as ModelMetric[] even though @@id([modelId]) makes it 1:1 in practice). The model.findFirst with orderBy: { metrics: { thumbsUpCount: 'desc' } } typechecks locally on the stale Prisma client but fails on a fresh one — CI hit the real generated types. Start the query from ModelMetric instead: orderBy the scalar directly, filter the related model by Checkpoint + ecosystem + Published, project the model + its top version through the metric. Same logical query, two-rows-deep instead of one. Updated test fixtures to wrap the model in a metric envelope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): showcase images in BLOCK_INIT.context for carousel UX New tRPC blocks.getShowcaseImages(modelVersionId): up to 6 published images for the version, de-duped, ordered by all-time reactionCount, with the standard gen-meta fields (prompt, negativePrompt, cfgScale, steps, seed, sampler) defensively extracted from the wide Image.meta JSONB. Public (showcase images are already public on the model page). IframeHost calls the query in parallel with getEffectiveCheckpoint and merges into BLOCK_INIT.context.showcaseImages so the block can render a carousel + populate gen params from the user's pick without an extra round-trip on mount. ModelSlotContext + ShowcaseImage type extended on the host side; the SDK mirror lands in a follow-up commit. 8 new tests covering reaction-sort, de-dupe, missing-metric fallback, and meta extraction across camelCase / A1111 PascalCase / malformed shapes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): block_user_subscriptions table + types Adds the schema substrate for user-controlled block installs: two scopes ('publisher_all_my_models', 'viewer_personal'), one table, three partial indexes (two for the listForModel hot paths, one for the management UI), and the wire shapes the new tRPC procedures will consume. * feat(blocks): BlockRegistry methods for user subscriptions + marketplace Adds four service-layer entry points consumed by the new tRPC procedures and management UI: - listUserSubscriptions: rows for the current user, both scopes, with the app block denormalised for rendering - upsertSubscription: idempotent write against the composite unique (userId, appBlockId, scope). Empty target arrays land as Postgres TEXT[] so the SQL array_length predicate normalises them back to 'no filter' at read time - deleteSubscription: owner-checked, idempotent on missing rows - listAvailable: marketplace listing with slot/query/cursor paging and install_count desc sort Tests cover idempotency, the owner gate on delete, target-array normalisation, and the listAvailable SQL shape. * feat(blocks): listForModel honours user subscriptions with viewer ctx Extends listForModel SQL with two new UNION branches: - source_rank 2: publisher_all_my_models subscriptions where Model.userId joins bus.user_id (transferring a model swaps which user's subs apply automatically) - source_rank 4: viewer_personal subscriptions where the current viewer's userId matches; anon viewers (-1 sentinel) match no rows Platform defaults move from rank 2 to rank 3 to slot between them. Each subscription branch carries target_model_types and target_base_models filters; empty arrays normalise to 'no filter' via array_length(...) IS NULL. The viewer branch carries three NOT EXISTS clauses so a higher-rank source already showing the same app_block + slot suppresses the duplicate. Caching: per-viewer correctness ranks higher than cache-hit rate in v1, so listForModel skips Redis entirely when viewerUserId is set. blocks.listForModel tRPC procedure now passes ctx.user?.id. Tests cover the four source ranks, the bus.user_id join, the -1 sentinel for anon, the cache disable on viewerUserId, and that two viewers don't see each other's cached results. * feat(blocks): tRPC procedures for subscriptions + marketplace Adds four procedures on blocksRouter: - listMySubscriptions (guarded) — both scopes for the current user, fail-soft to [] when the appBlocks flag is off - listAvailable (public) — marketplace listing with slot/query filter + cursor paging, fail-soft to empty when flag off - upsertSubscription (guarded) — validates settings through blockSettingsSchemaByBlockId and the 4KB cap, requires status='approved' on the target app block - deleteSubscription (guarded) — service-layer owner check, idempotent on missing rows Tests cover anon rejection, flag-off behaviour, app-block status gates, per-block-id settings validation (out-of-range buzz budget), and forwarded-argument correctness for both happy and error paths. * feat(blocks): /apps marketplace + per-app settings modal Adds three UI surfaces: - /apps marketplace page with slot-filter chips, debounced search, grid of AppBlockCard rendering name/description/slot/install count, gated on useFeatureFlags().appBlocks - AppBlockCard component used by the marketplace and (later) the installed page - AppSettingsModal: the per-app settings panel from the spec. Two scope toggles (publisher_all_my_models, viewer_personal), multi-select chips for target model types and base models, NumberInput for buzz_budget_per_gen, openResourceSelectModal integration for the default-checkpoint picker (reused from the checkpoint-selector handoff). Each scope toggle independently calls upsert / delete so the user can persist one scope without committing the other. SSR gate uses features.appBlocks + the standard session redirect. * feat(blocks): /apps/installed management page Lists the current user's subscriptions split into two sections — 'On models I own' (publisher_all_my_models) and 'On model pages I view' (viewer_personal). Each row carries: - block name + scope badge + filter chips (model types, base models) - inline enable/disable toggle (upsertSubscription with enabled flip) - settings gear → opens the same AppSettingsModal as the marketplace - trash → deleteSubscription with optimistic invalidate Empty states link back to /apps. SSR gates on features.appBlocks and the standard session redirect. * feat(blocks): publisher-subscription banner on model detail Owner-only Alert shown on the model detail page when one or more publisher_all_my_models subscriptions target this model (filtered client-side by model type; base-model filter applies server-side in listForModel). Each row offers: - 'Edit subscription' → /apps/installed - 'Disable for this model only' → installOnModel + toggleEnabled false (writes a per-model row that suppresses the subscription via NOT EXISTS in listForModel) The banner is hidden for non-owners and when the appBlocks feature flag is off. * feat(blocks): user-menu links to Apps marketplace + installed page Two new menu items in the user-state group, both gated on features.appBlocks (the same flag the slot rendering uses): - 'Apps' → /apps (newUntil: 2026-07-01) - 'Installed Apps' → /apps/installed The flag is currently availability: ['mod'] in feature-flags.service.ts, so non-mods won't see the items at all. Expanding to ['mod', 'member'] is the next rollout step per the handoff. * fix(blocks): typecheck cleanups for subscription paths - Meta on /apps and /apps/installed now sets deIndex (Meta's discriminated union requires either deIndex or canonical) - block-registry.service.ts: cast $queryRaw result to Row[] and annotate the .map callback (matches the listForModel pattern) - listUserSubscriptions: define an explicit SubRow type and cast the findMany result so the local typecheck stays green while the Prisma client is stale (CI regenerates the client on every build) * chore(blocks): silence editor diagnostics on subscription typings - subscription.schema.ts: replace deprecated zod .merge() with shape spread - subscription test mocks: type vi.fn() args/returns so mockResolvedValue payloads typecheck cleanly and mock.calls[N] index access works * feat(blocks): BlockRegistry.resolveBlockInstance for synthetic ids Adds a centralised lookup that translates a blockInstanceId of any kind — real install (bki_*), platform default (pdb_*), publisher subscription (bus_pub_*), viewer subscription (bus_view_*) — into the install-shape struct downstream code (token mint, settings update, workflow submit) consumes. Returns null when the instance doesn't resolve OR when the caller-supplied (modelId, slotId, viewerUserId) don't match what the source row would actually surface on listForModel. This is the cross-row gate that keeps an authenticated iframe from minting a token for a model the resolved source doesn't surface — for synthetic ids the row is per-user, not per-model, so the caller-supplied context is the only auth pin. Predicates mirror listForModel SQL (block-registry.service.ts:280-484): - mbi/bki_*: row.modelId == modelId, row.slotId == slotId, enabled, approved - pdb_*: enabled, slot matches, target_model_types filter, no install suppressor - bus_pub_*: scope, enabled, approved, manifest targets slot, Model.userId == bus.user_id, target_model_types + target_base_models filters, no install suppressor - bus_view_*: viewer == bus.user_id (anon never resolves), scope, enabled, approved, manifest targets slot, filters, AND cascading rank 1/2/3 suppressors (per-model install, publisher sub, platform default) 25 unit tests pin the cross-row re-validation for each source path, including malformed ids and rank-by-rank suppression for viewer subs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): block-tokens endpoint resolves synthetic instance ids Pre-fix, POST /api/v1/block-tokens did a raw modelBlockInstall.findUnique({where:{blockInstanceId}}) and 404'd for every blockInstanceId namespace except real installs (bki_*). Platform defaults (pdb_*), publisher subscriptions (bus_pub_*), and viewer subscriptions (bus_view_*) — all valid sources surfaced by listForModel — returned "Block install not found", blocking the iframe from minting a token. Replaces the lookup with BlockRegistry.resolveBlockInstance, which handles all four namespaces and re-validates the caller's (modelId, slotId) against the source row before mint. The validated modelId/slotId from the resolved row are what reach the JWT ctx — caller-supplied values in slotContext are never trusted for binding claims. slotContext is now schema-required to include modelId/slotId (the iframe host already sends both via useBlockToken.ts:96). Extra fields still flow through to BLOCK_INIT.context for display but are dropped from the JWT. The settings-scope publisher check at lines 411-418 keeps comparing against install.installedByUserId — for subscription sources this is set to bus.user_id (the subscription owner is the "publisher" for their own settings), which is the right semantic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): workflow + checkpoint paths resolve synthetic instance ids Wires BlockRegistry.resolveBlockInstance into the remaining two reads that fail for synthetic blockInstanceIds: 1. BlockRegistry.getEffectiveCheckpoint — called by the IframeHost pre-BLOCK_INIT to fill context.checkpoint. Now accepts modelId + slotId as the resolver's auth pin and reads publisher settings from the resolved source row (install/subscription/platform default). The tRPC procedure widens its input accordingly; the IframeHost forwards modelCtx.modelId and modelCtx.slotId. 2. resolveBlockCheckpoint (checkpoint.service.ts) — called from submitWorkflow/estimateWorkflow with claims.blockInstanceId. Now reads publisher settings via the resolver so a JWT minted for a bus_pub_* / bus_view_* / pdb_* synthetic id correctly resolves its publisher's default_checkpoint_version_id. The routers forward claims.ctx.slotId (stamped by block-tokens) to satisfy the resolver's auth pin. Adds one workflow router test that exercises submitWorkflow with a bus_pub_* JWT end-to-end and verifies the resolver was called with the correct (blockInstanceId, modelId, slotId) tuple from JWT ctx. updateSettings, toggleEnabled, and uninstallFromModel keep their direct modelBlockInstall.findUnique paths intentionally: those endpoints operate exclusively on real installs (bki_*) — subscription settings have their own write path via blocks.upsertSubscription, and platform defaults aren't settings-writable at all. A synthetic id reaching those endpoints is a client bug; the 404 they return today is the correct fail-closed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(blocks): type vi.fn() args on block-tokens test mocks * fix(blocks): showcase reads ImageResourceNew (legacy table is empty) * feat(blocks): inherit clipSkip from showcase image meta Mirrors what the platform's Remix flow extracts from Image.meta (getMediaGenerationData reads meta.clipSkip ?? meta['Clip skip']). Forwards through the block-side schema, workflow input builder, ShowcaseImage type, and the SDK-mirror in components/AppBlocks. * fix(blocks): prefer meta-recorded gen dims over image file dims in showcase Many showcase images are generated at one resolution (e.g. 832x1216) and upscaled offline to a higher resolution (e.g. 2496x3648) before being uploaded. Image.width/Image.height reflect the post-upscale file; meta.width/meta.height reflect the actual generator output. The block was reading file dims, so the user picking a showcase image got a generation at ~3x area — even with seed/cfg/steps/sampler/ clipSkip identical, the composition diverged noticeably from the showcase (real-world: workflow 8753561-20260525223849768 ran at 1408x2048 from a 2496x3648 image whose meta said 832x1216). Falls back to file dims when meta lacks width/height (older images, non-SD pipelines). Block-side clamp still scales anything over 2048. * feat(blocks): block_buzz_attribution schema + BlockAttribution type One row per buzz purchase originated inside an App Block. Drives the publisher revenue-share payout pipeline. block_instance_id is TEXT (not FK) because it can resolve to mbi_/bus_pub_/bus_view_/pdb_ — the scope column tells the reader which surface owns the id. app_owner_user_id and rate_card_version are snapshot at attribution time so past revenue stays stable when ownership or rate cards change. Includes a share-sum CHECK constraint (provider_fee + platform_share + app_owner_share = usd_amount) so arithmetic bugs in the rate-card calculator surface at write time. Adds the BlockAttribution schema with deriveScopeFromInstanceId + encode/extract helpers used by the modal, the iframe host, and all three payment-provider webhook paths. * feat(blocks): rate card v1 with placeholder publisher share pcts Defines RATE_CARD_V1 (active) with the four scope-based publisher cuts agreed during planning: 20% / 20% / 25% / 0%. computeRateCardSplit takes gross + provider fee + scope and returns the three-way split that satisfies the migration's share-sum CHECK constraint. Important: percentages are PLACEHOLDER pending monetization-leadership sign-off — soft-launch only. The handoff doc enumerates the open items. Rate cards are never mutated in place — new versions = new exported constants, past attributions pay out under their snapshot. 10 unit tests cover clean splits per scope, self-purchase / internal- owner zero overrides, fractional-cent flooring (publisher never overcollects), negative-gross clamping, and the active-card invariant for every scope. * feat(blocks): BlockBuzzAttribution.record service + void path Writes one block_buzz_attribution row per (paymentTransactionId, appBlockId) — idempotent via the unique constraint. Resolves the app owner from OauthClient at write time and snapshots userId onto the row so payouts are stable when ownership later changes. Self-purchase wash (purchaser == publisher) writes the row with status='voided', voided_reason='self_purchase', publisher share = 0 — audit-friendly without erroring the buzz credit. Internal app owners get the same zero-share treatment via the rate card. P2002 idempotency uses duck-typing on err.code instead of instanceof Prisma.PrismaClientKnownRequestError so the path works even when the Prisma client is stale at runtime (CI worktrees). voidAttributionsForPayment flips rows to voided on refund/chargeback — used by the upcoming refund webhook integration. REFUND_WINDOWS_DAYS holds the per-provider refund windows for the confirm-pending cron job (next phase). 12 unit tests cover: per-scope share math, self-purchase voiding, idempotent retry, missing-app guard, modelId / buzzTxId flow-through, audit log emission, void+refund path. * feat(blocks): Stripe webhook records attributions + voids on refund payment_intent.succeeded now writes a block_buzz_attribution row after the buzz credit lands, when the payment-intent metadata carries block* keys. Skipped silently on test-mode events (livemode=false), skipped silently when no attribution keys are present (the steady- state for every non-block buzz purchase) — no regression risk on unrelated buzz flows. Provider fee is pulled from the charge's balance_transaction so the publisher cut comes off the actual Stripe net, not gross. Falls back to 0 if the expansion fails — share-sum CHECK still holds because the calculator constructs (fee, platform, publisher) from a single gross. charge.refunded / charge.dispute.created now flip matching attribution rows to voided alongside the existing referral-kickback revoke. If the row was already paid out, status='voided' is still set and the payout reconciliation job claws back from the next payout. Attribution write failures are logged but never fail the webhook — the buzz credit has already happened, and Stripe's retry policy plus the UNIQUE constraint make the write idempotent on retry. 12 schema tests cover the metadata roundtrip, prefix→scope resolver, and corrupt-input rejection. * feat(blocks): Paddle webhook records attributions on buzz purchase processCompleteBuzzTransaction now writes a block_buzz_attribution row after the buzz credit lands, when the price-level customData carries block* keys. Same skip-on-no-attribution + idempotent-on-retry shape as the Stripe path. Provider fee left at 0 for v1 (TODO — paddle's SDK doesn't surface fees on the line item; needs a transactions.get). Refund/chargeback void path is NOT wired for Paddle because the existing webhook handler doesn't subscribe to TransactionAdjusted / adjustment.created events. Paddle is in maintenance mode per the header comment in webhooks/paddle.ts — no new signups flow through it. Reconciliation will run against the Paddle adjustments API periodically; the void call should land in that path when it ships. Captured in the handoff doc as a known gap. * docs(blocks): document why NOWPayments has no buzz attribution NOWPayments uses shared per-user deposit addresses + order_id = 'user:{userId}' with no per-purchase metadata bag, so attribution can't ride through to the IPN webhook. Two ways to wire it up are called out (session table or per-purchase addresses) but both are out of scope for v1. In-block crypto buzz purchases will credit buzz normally but write no attribution row — publishers don't earn share on those. Captured here so the next implementer sees the constraint before chasing it. * feat(blocks): IframeHost passes attribution into BuyBuzzModal OPEN_BUZZ_PURCHASE now derives an attribution payload from the install context (appId, appBlockId, blockInstanceId + prefix-resolved scope + optional modelId) and threads it through: IframeHost → BuyBuzzModal → BuzzPurchaseLayout → BuzzPurchaseImproved → Stripe paymentIntent metadata. The iframe never supplies these fields itself — host-derived only — so a malicious block can't forge attribution to a different app or publisher. Unknown blockInstanceId prefix → undefined attribution → no row written (defensive fail-closed). To enable this, BlockInstall / BlockInstallRecord gained an appBlockId field (the app_blocks.id, distinct from the manifest block_id) and listForModel's SQL selects it from all four union arms. The deriveScopeFromInstanceId resolver now handles both mbi_ and bki_ prefixes (legacy unique-column on the same install row) in lockstep with BlockRegistry.resolveBlockInstance. 13 schema tests + the rest of the buzz-attribution test suite stay green. The 3 pre-existing checkpoint-service failures are unrelated (stale Prisma in the worktree). Paddle path is left without attribution threading in the modal — the current Paddle flow in BuzzPurchase.tsx has its Stripe handler commented out and paddle is in maintenance per the webhooks/paddle.ts header. Stripe is the only live attribution path for v1. * feat(blocks): daily cron promotes pending attributions to confirmed Runs at 03:15 UTC daily, one updateMany per provider. Stripe gets a 30-day window, Paddle 14d, NOWPayments 1d (window constants live in buzz-attribution.service.ts and are imported here so the cron and the rate-card stay in lockstep). Idempotent on the WHERE clause — only filters status='pending'. Already-confirmed/voided/paid_out rows are inert. 3 unit tests assert the per-provider cutoff math + status invariant. The refund void path was wired in phase 4 (Stripe webhook); this job is what makes the confirmed → paid_out pipeline work on the happy path. * feat(blocks): bulk-payout stub job + handoff for payout pipeline Runs Mondays 09:30 UTC. Currently writes NOTHING — just aggregates status='confirmed' rows by app_owner_user_id and logs the queue depth + dollar total to Axiom for observability. Automation of the actual payout is blocked on monetization-leadership decisions, all enumerated in the job's header doc: - Money flow: integrate with creator-program cash bank (UserPaymentConfiguration + Tipalti, couples to compensation pool cap logic) OR mint a separate Tipalti payment via payToTipaltiAccount (skips pool guards + 1099 routing). - UserPaymentConfiguration prerequisite + missing 'earnings ready' notification for App Blocks publishers. - Refund clawback on paid_out rows (Tipalti adjustment shape). - 1099 / tax reporting — only flows through the cash bank path. Until those land, publishers see 'confirmed' rows accumulate on the dashboard but no auto-disbursement fires. Leadership can batch-process the queue manually using the Axiom log + the per-publisher breakdown in the log payload. * feat(blocks): blocks.getMyRevenue + blocks.getMyApps tRPC procedures Two guardedProcedure queries gated on the App Blocks feature flag: blocks.getMyRevenue({ appBlockId?, from?, to? }) Aggregate revenue summary for the caller across pending/confirmed/ paid_out/voided buckets, plus the top 5 earning apps and the 50 most recent attribution rows. Service filters by appOwnerUserId so a request with someone else's appBlockId returns empty. blocks.getMyApps() Owned apps + lifetime confirmed+paid_out revenue per app. One groupBy across all apps so the request stays sub-linear. Service-side helpers (getRevenueForOwner, getRecentAttributionsForOwner) fire 4-5 small aggregate queries in parallel — the bba_publisher_dashboard_idx on (app_owner_user_id, attributed_at DESC) keeps each one cheap. Pages render off these two endpoints in the next phase. * feat(blocks): /apps/revenue + /apps/[appBlockId]/revenue pages Two new pages reading from blocks.getMyRevenue / blocks.getMyApps: /apps/revenue 4 summary cards (pending / confirmed / paid out / voided), Top 5 earning apps (links into the per-app page), recent attributions table. /apps/[appBlockId]/revenue Same summary cards filtered to one app, recent attributions table scoped to that app. Owner check on the client surface: myAppsQuery.data.find((a) => a.id === appBlockId); if not in the owner's list we fail closed with NotFound. Server-side service filter (appOwnerUserId in the WHERE) is the actual auth gate — the UI NotFound just keeps the intent legible. Local type aliases on the page level because RouterOutputs isn't exported in this codebase + the worktree's stale Prisma client reduces inferred query data to {}. CI will type-narrow correctly when the client regenerates. Out of scope for v1 (deferred per the spec): timeseries chart, date range picker, CSV export, scope breakdown stacked bar. * feat(blocks): App Revenue nav link + earning chip on marketplace cards User menu (gated on features.appBlocks) gets a new 'App Revenue' entry below 'Installed Apps', linking to /apps/revenue. Green IconCurrencyDollar to visually distinguish the earnings flow from the install management flow. Marketplace cards (/apps) gain an 'Earning $X.XX' badge when the viewer owns the app and has lifetime confirmed+paid_out share > 0. Chip is suppressed when ownedEarningCents is undefined (not owned) or 0 (owned but no earnings yet) — the upsell is 'you're earning', not 'you could earn'. ownedEarningCents flows from blocks.getMyApps which is guarded by guardedProcedure, so non-owners never see anyone else's earnings. * feat(blocks): prometheus counter for buzz attribution writes Adds civitai_app_block_buzz_attribution_total with provider/scope/ status labels. Incremented in BlockBuzzAttribution.record after the DB write succeeds — best-effort try/catch so metric infrastructure issues never back-pressure the webhook. Audit logs (logToAxiom 'block-buzz-attribution' channel) already landed in phase 3 + phase 4. With the counter in place, ops gets: - per-provider funnel (Stripe vs Paddle attribution-write volume + scope mix) - self-purchase wash visibility (status='voided' rows show up immediately on the metric, so a spike implies someone gaming their own app) - confirmed/voided lifecycle dashboards are still possible via follow-up counters (next change — pending → confirmed promotion + refund void path could each get their own). The promised pending_share_cents gauge from the original handoff is not wired in this commit — it requires a periodic exporter against the dbRead aggregate query, and the bulk-payout stub job already logs that same number for ops via Axiom. Add the gauge once the payout pipeline lights up so dashboards can show 'money waiting to pay out' alongside 'money paid out'. * fix(blocks): add missing OauthClient.buzzAttributions back-relation Prisma schema validation rejected BlockBuzzAttribution because the `app` relation lacked an opposite-side back-reference on OauthClient. AppBlock already had buzzAttributions; OauthClient was missed when the subagent landed the model. Caught by Tekton typecheck step (P1012). * ci: re-trigger preview build * chore: trigger preview re-run after infra-side failure * fix(blocks): break Prisma groupBy type back-propagation in bulk-payout Prisma's groupBy uses constrained generic inference: a direct `as GroupRow[]` cast on the await result back-propagates into the args type as an intersection (`& GroupRow[]`), which then fails to validate the args object. Cast through `unknown` to break the back-propagation. Caught by Tekton typecheck (TS2345). Local typecheck didn't surface it because db:generate is broken in the worktree. * feat(blocks): rate card v2 + payout routing/clawback recommendations Rate card v2 lands the recommended starting percentages: per_model_install: 20% -> 15% (most counterfactual) publisher_all_my_models: 20% -> 15% (same) viewer_personal: 25% (kept; most incremental) platform_default: 0% (kept; mod-promoted) V1 stays defined for history. ACTIVE_RATE_CARD now points at V2. Start lower; raising via V3 later is politically easier than lowering after a public announcement. bulk-payout-block-attributions.ts header documents the recommended implementation path: route money through creator-program cash bank (1099 + existing UserPaymentConfiguration), carry-forward debt for refund clawback (affiliate-network standard, transparent ledger). Job remains a stub pending monetization leadership sign-off. * feat(blocks): autoclaim daily boost reward when user balance is short Submit flow now opportunistically claims the daily boost (25 blue Buzz, one per UTC day) when a user clicks Generate on a block and their actual Buzz balance would be short — but only when the claim would close the gap. If the boost wouldn't be enough on its own, the claim is skipped so the one-per-day reward isn't burned on a still-hopeless submit. Gate is conservative: precheck balance+details, only call apply() when (1) boost is unclaimed today, (2) current spendable balance < cost, and (3) balance + awardAmount >= cost. apply() is idempotent (Redis Lua dedups per UTC day) and any failure is logged + swallowed so submit still proceeds and surfaces the existing Top-Up CTA. Snapshot grows an optional `autoClaim` field that the iframe can use to surface a "+25 daily boost claimed" notice; the SDK type mirror is bumped to 0.5.0 in a sibling repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(app-blocks): W3 v0 — manifest-driven settings + generic form renderer (#2334) * feat(blocks): W3 v0 phase 1 — manifest-driven settings meta-schema + generic validator Adds the meta-schema that the W2 webhook handler will validate manifests against on push, plus the runtime validator that replaces the per-block-id schema map at every settings call site. manifest-settings.meta.schema.ts — record<snake_case_key, SettingField>: - discriminated union over type=number|string|boolean - scope=publisher|viewer + requires_scope gating - widget hints (number/slider/resource_picker, text/textarea/select, toggle) - cross-field checks: min<=max, default in range, select needs enum, RegExp parses settings-validator.service.ts — validateBlockSettings({manifest, input, scopes, forScope}): - wrong-scope fields silently skipped (single fn validates either side) - requires_scope filter for app-scoped feature gating - defaults applied, unknown keys stripped without leaking which were unrecognized - explicit null preserved when field declares default:null (resource picker case) - TRPCError(BAD_REQUEST) per offending field so install-form UI can surface inline 50 unit tests across both files exercise the happy paths, scope filtering, null handling, and every per-type failure mode. Phase 2 (call-site migration + deleting blockSettingsSchemaByBlockId) lands separately so this commit can stand on its own — W2 can import manifestSettingsSchema for its webhook validator before phase 2 ships. * feat(blocks): W3 v0 phase 2 — migrate call sites to generic manifest validator Replaces the per-block-id settings schema map with manifest-driven shape validation at every settings write. Deletes settings.schema.ts (the in-tree typed schemas + blockSettingsSchemaByBlockId lookup). Settings call sites migrated: - block-registry.installOnModel: fetch manifest + approvedScopes, run validateBlockSettings(forScope=publisher) + checkpoint cross-row check. - block-registry.updateSettings: same. - block-registry.upsertUserSettings / getUserSettings: param + return now Record<string, unknown> instead of BlockUserSettings. - block-registry.getEffectiveCheckpoint: read raw publisher / viewer values with typeof guards (validation already enforced at write time). - blocks.upsertSubscription router: fetch manifest, derive forScope from subscriptionScope, run generic validator. - blocks.updateUserSettings router: accept generic settings record, resolve install via resolveBlockInstance for manifest+scopes, validate with forScope=viewer, keep the checkpoint cross-row check. - checkpoint.service.resolveBlockCheckpoint: read raw checkpoint_version_id + default_checkpoint_version_id with typeof guards. The static manifest is the contract; cross-row checks (checkpoint must exist + share ecosystem) stay as adjacent special-cases since they need DB reads the meta-schema can't express. Third-party apps in v1 will be able to add settings without a civitai-side PR — this is the v0 substrate for that. Phase 1's 50 manifest-settings + settings-validator tests still pass. Block-tokens, showcase, workflow, attribution.schema, rate-card unit tests unaffected (100 tests green across the touched modules). * deps: add @civitai/app-sdk + @civitai/blocks-react W3 + W4 work imports from @civitai/app-sdk/blocks (ManifestSettings, SettingField, app-storage message types) and @civitai/blocks-react/ui (SettingsForm) + @civitai/blocks-react hooks (useAppStorage). Both packages publish from civitai/civitai-app-starters PR #11. DO NOT MERGE this commit until the npm publish lands: npm view @civitai/app-sdk@0.6.0 version # → 0.6.0 npm view @civitai/blocks-react@0.4.0 version # → 0.4.0 After publish + merge, `pnpm install` regenerates pnpm-lock.yaml. W4 (zach/w4-kv-datastore) and W2 (zach/w2-phases-2-7) inherit these deps when they rebase onto this commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(app-blocks): W2 v0 phases 2-7 — civitai-web service layer + Submit UI + schema (#2336) * feat(blocks): W2-v0 Phase 3 — Forgejo client + apps-pipeline + webhooks Three pieces of plumbing that connect Forgejo pushes to the per-app deploy on dp-1: - src/server/services/blocks/forgejo.service.ts REST wrapper for Forgejo: createRepoFromTemplate, addCollaborator, ensurePushWebhook, getRawFile, setCommitStatus. Talks to forgejo-http.forgejo.svc.cluster.local in-cluster or https://forgejo.civitaic.com from PR-preview envs. - src/server/services/blocks/apps-pipeline.service.ts Two cross-cluster k8s API helpers. triggerBuild() POSTs a PipelineRun to dc-02-a's Tekton via a mounted kubeconfig (APPS_TEKTON_KUBECONFIG). triggerApply() POSTs an apply Job to dp-1's civitai-apps namespace via the in-pod default-SA token (RoleBinding lives in datapacket-talos clusters/production/apps/civitai-apps/rbac.yaml). - src/pages/api/internal/blocks/git-push.ts Forgejo push webhook. HMAC-verifies, requires app-blocks-enabled Flipt flag, looks up app_blocks row by slug, fetches the manifest at the new SHA, validates against BlockManifestValidator + canonical iframe.src host pattern, upserts, triggers build, writes pending commit status. - src/pages/api/internal/blocks/build-callback.ts Tekton finally-task callback. HMAC-verifies the shared secret, flips commit status, triggers the apply Job, updates current_version_deployed_at. The DB column lives in the Phase 4 migration (next commit). Env additions (all optional so envs without the platform layer still boot): FORGEJO_BASE_URL, FORGEJO_ADMIN_TOKEN, FORGEJO_WEBHOOK_SECRET, BLOCK_BUILD_CALLBACK_SECRET, APPS_TEKTON_KUBECONFIG, APPS_TEKTON_NAMESPACE (default tekton-builds), APPS_KUBE_NAMESPACE (default civitai-apps), APPS_DOMAIN (default apps.civitaic.com). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W2-v0 Phase 4 — blocks.submitApp + Submit UI + schema - prisma/schema.full.prisma + new migration 20260526200000_app_blocks_repo_versioning adds current_version_sha, current_version_deployed_at, repo_url to app_blocks. All nullable — hackathon rows pre-W2 stay valid until W12 cutover. Per CLAUDE.md gotcha #14, the migration must be applied manually via psql. - src/server/routers/blocks.router.ts — adds the submitApp mutation (civitai-team gated). Creates a Forgejo repo from civitai-apps/starter via the new forgejo.service, attaches a push webhook pointing at /api/internal/blocks/git-push, and inserts a pending app_blocks row with apb_<ULID> id (mirrors existing hackathon convention; the v1 developer-endpoint at /api/v1/developer/block-manifests uses ab_). - src/server/utils/app-block-ids.ts — newUlid() now public so callers needing a non-standard prefix can compose their own. - src/pages/apps/submit.tsx — Civitai-team-only form. Slug + OauthClient picker + description. Success state shows repo URL, clone command, public URL, and the new appBlockId. - src/pages/apps/index.tsx — Submit App button on the marketplace header, mod-gated client-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(app-blocks): W4 v0 — isolated KV datastore (cnpg-cluster-apps) (#2335) * feat(apps): AppStorageProvisioner + appsDb client (W4-KV-v0 P2) Substrate for the App Blocks KV datastore. Adds the connection to the new cnpg-cluster-apps cluster (datapacket-talos commit eb86f3a33) and the idempotent per-app schema/role provisioner. tRPC procedures + SDK + IframeHost handlers land in later phases. - `APPS_DATABASE_URL` server-schema env (optional — appsDb is null in environments that don't have the apps cluster wired). - `getClient({ instance: 'apps' })` extension; new appsDb singleton in `src/server/db/appsDb.ts` mirrors the notifDb pattern. - `sanitizeAppSlug` / `isValidAppSlug` / `appSchemaIdent` / `appRoleIdent` in `src/server/utils/apps-slug.ts`. Regex `^[a-z][a-z0-9_]{2,40}$` is the load-bearing safety boundary — identifiers can't be parameterized in pg so DDL leans entirely on this gate. - `AppStorageProvisioner.{provision,deprovision,getQuota}` in `src/server/services/apps/storage-provision.service.ts`: * Schema, kv table (with generated size_bytes column), quota table, trigger function, trigger, role + grants, default privileges, seed row — all inside a single client.query('BEGIN') / 'COMMIT'. * Trigger pulls app_block_id from session-local `app.current_app_block_id` so the tRPC procedure layer scopes writes via SET LOCAL inside the same txn. Missing GUC no-ops to avoid hard-failing the user path. * Idempotent — IF NOT EXISTS on every DDL + DO-block guards on role creation; ON CONFLICT on the quota seed. * Slug + appBlockId validated before pool.connect() so bad input can't even reach the wire. - Unit suites for the slug helper (28 assertions) and the provisioner (mocked pg client; checks txn boundaries, identifier quoting, the parameterized quota seed, rollback on mid-DDL failure, and the getQuota happy/empty/missing-schema paths). Out of scope for this phase: tRPC procs (P3), SDK hook (P4), IframeHost handlers (P5), metrics + audit (P6), backfill + hackathon provision (P7). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(apps): apps.storage.* tRPC procedures + tests (W4-KV-v0 P3) Five host-mediated procedures behind the block JWT: - `apps.storage.get(key)` — null for anon viewers, scoped to (block_instance, user). - `apps.storage.set(key, value)` — 64KB per-value cap, 50MB per-app quota gate (uses the per-row size delta on update so a shrink doesn't falsely trip), single-connection SET LOCAL → INSERT … ON CONFLICT. - `apps.storage.delete(key)` — same connection-scoped txn shape. - `apps.storage.list({ prefix, limit, cursor })` — keys-only, cursor pagination, LIKE wildcards in user-supplied prefixes are escaped. - `apps.storage.getQuota()` — surfaces used/row counts + the v0 caps so client UIs don't hard-code the ceiling. `resolveStorageContext` is the shared gate — verify the JWT, validate the slug via the regex helper, look up the AppBlock by (appId, blockId) to enforce status='approved' and pull the appBlockId for quota keying, parse userId from the sub. Every gate emits an `app_blocks_storage_ops_total` counter increment with op + outcome so dashboards can pin failure mode without log-side correlation. Mounted as `apps` (new top-level tRPC router) — sibling to `blocks`. v1 will extend `apps.*` with `sql.query` + `migrate.run`; the namespace is intentional even though there's only one sub-router today. Tests (vi-mocked pool + verifier + dbRead + provisioner): - Flag dark, bad token, missing/unapproved AppBlock, malformed slug. - Anon-viewer null returns on get/list; UNAUTHORIZED on set/delete. - get returns DB value + uses the correct schema-quoted SQL. - set: per-value cap, quota gate including the shrink-allowance via net delta, happy-path transaction shape (BEGIN → SET LOCAL → INSERT → COMMIT) plus client release. - delete reports rowCount > 0 vs 0. - list paginates only when the page filled; LIKE-special chars escaped. - getQuota proxies the provisioner snapshot + surfaces v0 limits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): IframeHost storage handlers (W4-KV-v0 P5) Wires the five `APP_STORAGE_*` bridge messages into the new `apps.storage.*` tRPC procedures. Same pattern as the existing workflow bridge — each handler validates the incoming postMessage shape, calls into the procedure with the block token, and posts a result back to the iframe with the same requestId. - get / list / getQuota — imperative fetch via `trpc.useUtils()` (procedures are tRPC `.query()` but the call site is a one-shot message handler, not a reactive subscription). - set / delete — mutations. - Errors are surfaced via `error: <string>` on the result payload so the SDK hook can reject; handlers NEVER throw upward and strand the postMessage round-trip. - `storageErrorMessage` keeps surfacing conservative — uses the TRPCClientError `.message` when available, falls back to a generic string. The iframe is untrusted so we don't leak stack traces. - List handler clamps user-supplied `limit` to [1, 200] and rehydrates `updatedAt` from Date → ISO on the wire (the SDK rehydrates on the block side). Paired with the apps.storage router (P3) + the useAppStorage SDK hook (P4, civitai-app-starters branch zach/w4-storage-sdk). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(apps): storage latency histogram + per-write audit (W4-KV-v0 P6) Closes the metrics + audit-log bullet on the W4-v0 acceptance list. - `civitai_app_app_blocks_storage_latency_seconds{op}` histogram — buckets 1ms → 2.5s, registered HMR-safe with the same pattern as the bitdex shadow-query histogram. - Every `apps.storage.*` procedure starts a timer on entry, ends it in `finally`. Failures + happy paths both observe the timing so error bursts are visible in latency dashboards (not just the counter). - Per-write audit on `set` (every success → `event: 'set'` with appBlockId / blockInstanceId / userId / key / sizeBytes / isInsert) and `delete` (only when a row was actually removed). Streams via the existing `logToAxiom` path under `app-storage-trpc` log name — Loki-side alerts can compute the >10/s sustained-write abuse signal the handoff calls out without us holding rate counters in-process. - Quota-exceeded log line was already in place from P3 — the histogram + per-write audit are the additions here. The hourly per-(app_block_id) used_bytes / row_count gauges from the handoff are deferred to a follow-on commit (probably a CronJob in datapacket-talos rather than per-pod polling). Counters + histogram + audit log are enough for the v0 acceptance gate; gauges are nice-to- have for the marketplace UI in W1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(apps): admin backfill endpoint for KV provisioning (W4-KV-v0 P7) `GET /api/admin/apps-storage-backfill?token=$WEBHOOK_TOKEN` — walks every `app_blocks.status='approved'` row, calls `AppStorageProvisioner.provision({ appBlockId, slug })`. Idempotent. Dry-run by default; pass `?apply=true` to actually provision. Use cases: - W2 webhook never fires (preview environment, manual SQL inserts): one-shot to bring the apps DB in sync. - cnpg-cluster-apps reset (DR, schema-explosion cleanup, etc.): re-provision everything in one call. - Manual one-off: `?appBlockId=apb_xxx&apply=true` targets a single app. - Recommended operator cron: 1h heartbeat against `?apply=true` until W1's submission queue lands — closes the gap between W2 webhook retries. 503s cleanly when `APPS_DATABASE_URL` is unset so PR previews that don't have the apps DB wired stay deployable without hitting this endpoint. Pairs with the v0 ship handoff in datapacket-talos (claudedocs/app-blocks-w4-kv-datastore-v0-shipped-2026-05-27.md) which documents the four operator preflight steps + the end-to-end smoke. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): regen pnpm-lock.yaml for @civitai/app-sdk + @civitai/blocks-react W3 added the two SDK packages to package.json (commit bb0e98e95) but didn't regen the lockfile, so Tekton (which uses --frozen-lockfile) failed every preview build since W3 merged. Resolves to @civitai/app-sdk@0.6.0 + @civitai/blocks-react@0.4.1 from npm (0.4.1 is the workspace: leak patch). No other deps changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(deps): add yaml package — required by W2 apps-pipeline.service W2's apps-pipeline.service.ts imports `* as YAML from 'yaml'` for envsubst-style manifest manipulation, but yaml wasn't declared in package.json. Tekton typecheck failed with TS2307. Adds yaml@^2.8.1 (already a transitive dep at that version; promoting to direct). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): verify HMAC over raw bytes, not re-serialized JSON (#2338) * fix(blocks): verify HMAC over raw request bytes, not re-serialized JSON Forgejo signs the pretty-printed Go-encoded body it sends, with `\n ` indentation. Next.js's bodyParser parses the JSON into req.body, and the handler's `JSON.stringify(req.body)` produces compact JSON with no whitespace — the byte sequences differ, so the HMAC never matched. Repro on civitai-pr-2319: every Forgejo push to civitai-apps/* logged `401 Bad signature` in Forgejo's hook_task table; receiver received the request and rejected it. Fix: disable Next's bodyParser on git-push.ts + build-callback.ts, read the raw stream into a Buffer, verify HMAC against the raw bytes, then JSON.parse for the handler's logic. Caps body size to bound surface. Same bug class as the Stripe / Coinbase / Paddle webhooks already in this repo — they all use `bodyParser: false` with a manual stream reader for this reason. Unblocks App Blocks W12 cutover gap #1. * fix(blocks): triggerBuild via HMAC trigger receiver, drop kubeconfig path W2-v0's design had civitai-web parse a kubeconfig and POST PipelineRuns directly to dc-02-a's API server. That doesn't work — dc-02-a's API is loopback-only (SSH-tunnel for operators) and not reachable from dp-1 pods. The kubeconfig also used cert-auth which the code couldn't parse. Replace with a small HMAC-protected receiver on dc-02-a (`app-blocks-trigger`, see datapacket-talos/claudedocs/app-blocks-tekton-trigger/). civitai-web POSTs JSON to the receiver via the existing dp-1 → dc-02-a VPN proxy (`wireguard-proxy-service:8088`), receiver validates HMAC, creates the PipelineRun with its own in-pod ServiceAccount. Trade-off: one more piece of cluster infra (small Python receiver, ~150 lines). But: no kubeconfig juggling, no dc-02-a API exposure, standard HTTP+HMAC pattern that's easier to debug than k8s API impedance mismatches. Env schema changes: - Removed APPS_TEKTON_KUBECONFIG, APPS_TEKTON_NAMESPACE - Added APPS_TEKTON_TRIGGER_URL, APPS_TEKTON_TRIGGER_SECRET Unblocks App Blocks W12 cutover gap #3. * chore: re-trigger pr-preview build (deploy-dev label was added post-merge) * chore: re-trigger pr-preview with preview-db/prod label (use prod DB for app_blocks) * chore: retrigger pr-preview after buildkit lock contention (792b6 failed) * feat(app-blocks): migrate to civit.ai domain (single-level wildcard) (#2340) * feat(app-blocks): migrate domain to civit.ai (single-level wildcard) `*.apps.civitaic.com` would be two levels under civitaic.com — CF Universal SSL is single-level wildcard only, so each per-app subdomain would need a paid Cloudflare Advanced Cert (~$10/mo each) OR DNS-only routing without CF's edge protection. Switching to `<slug>.civit.ai` (single-level on a dedicated zone) is covered by CF Universal SSL for free. Changes: - APPS_DOMAIN default: apps.civitaic.com → civit.ai - submit.tsx: replace 4 hardcoded refs (UI display strings) - apps-pipeline.service.ts: pass APPS_DOMAIN to apply Job env so the template ConfigMap can interpolate it into IngressRoute + Certificate - forgejo.service.ts: doc comment forgejo.civitaic.com → forgejo.civitai.com Paired with datapacket-talos changes (ExternalDNS domain-filter + app-templates ConfigMap + Forgejo IngressRoute migration to civit.ai + GitHub-org oauth2-auth gate on Forgejo). * fix(blocks): apply Job image bitnami/kubectl:1.34 → alpine/k8s:1.34.0 docker.io/bitnami/kubectl:<ver> returns NotFound (Bitnami images retired 2025-Q4). The apply Job stuck ImagePullBackOff after callback. Switch to alpine/k8s:1.34.0 which has bash + kubectl + envsubst — matches the template's `bash -c` + envsubst usage. * feat(blocks): auto-add per-app host to OauthClient.allowedOrigins on submitApp (#2344) Without this, the first Forgejo push for a newly-submitted app rejects the manifest with `400 iframe.src rejected: origin https://<slug>.<APPS_DOMAIN> not in OauthClient.allowedOrigins`. Operator had to manually run an UPDATE on the OauthClient row before the build pipeline could fire (bit twice during the W12 cutover). Read allowedOrigins on the existing OauthClient + append the new host if not already present. Idempotent (won't dup on resubmit). Skipped on unique-constraint conflict path (existing app block) so the conflict error surfaces clearly. * chore: retrigger pr-preview to pick up #2340 + #2344 * feat(blocks): /apps/submit can auto-create OauthClient inline Until now the form required picking an OauthClient owned by the submitter. `oauthClient.getAll` filters by `userId: ctx.user.id`, so a first-time moderator with zero owned clients hit a hard "No OAuth clients found" wall and had to drop into psql before the form would accept anything. The implicit shape was also off: each block should have its own OauthClient (allowedOrigins + scopes diverge per app), so the list-existing flow was implicitly encouraging client-reuse across unrelated blocks. Make oauthClientId optional in `blocks.submitApp`. When absent, insert a fresh public client scoped to this app: id <ulid>-app-block-<slug> (matches hackathon pattern) secret null (block iframes can't hold secrets) name description ?? "App Block: <slug>" redirectUris [] (App Blocks use the JWT path, not code flow) allowedOrigins [https://<slug>.<APPS_DOMAIN>] isConfidential false userId ctx.user.id The post-create allowedOrigins-append from #2344 becomes a no-op for auto-created clients. UI swaps the Select for a SegmentedControl ("Create new" / "Use existing"), defaulting to "Create new" and disabling "Use existing" when the user has zero owned clients. Both modes share the rest of the form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: retrigger pr-preview (95t8j hung at #33 cache export) * chore: retrigger pr-preview * chore: retrigger pr-preview * chore: retrigger pr-preview * chore: retrigger pr-preview * ci: retrigger pr-preview after replace --force deploy fix Verifies the fix for the env value/valueFrom merge break that failed pr-preview-t6t9g (APPS_TEKTON_TRIGGER_SECRET flip). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 1+2 — publish-request flow backend Lays the substrate for the App Blocks W1 publish-request flow: dev uploads a ZIP via the UI, civitai-web stores it on ssd-minio-backups MinIO, computes manifest + file diff summaries vs the previous approved version, and inserts an app_block_publish_requests row for moderator review. Replaces the W12 direct-Forgejo-push UX (which exposed Forgejo to developers). Under W1, devs never see Forgejo: mod review happens in /apps/review (Phase 3), and on approve the platform uploads to Forgejo server-side and the existing Tekton build chain fires. Phase 1 (data model): - prisma/schema.full.prisma — AppBlockPublishRequest model with status enum, FK to AppBlock (nullable, populated on first approve), 4 composite indexes (mod queue, per-app history, my-submissions, slug lookup). - prisma/migrations/20260528170000_w1_publish_requests/migration.sql — manually applied to cnpg-cluster-nvme0 prod 2026-05-28 per CLAUDE.md gotcha #14. Includes 4 CHECK constraints (status enum, review-pair, rejection-reason-required, approved-forgejo-sha-required) and an updated_at trigger. Phase 2 (submit-version backend): - src/env/server-schema.ts — BUNDLE_S3_* env vars (optional so envs without W1 wiring still boot). - src/utils/bundle-s3.ts — S3Client + bucket getter for the ssd-minio-backups MinIO endpoint with bucket-scoped credentials. - src/server/schema/blocks/publish-request.schema.ts — submitVersion + withdrawRequest input shapes; 50 MiB bundle cap (67 MiB pre-decode base64 cap); 2000 files / 10 MiB-per-file in-bundle caps. - src/server/services/blocks/publish-request.service.ts — pipeline: decode bundle → parse ZIP (jszip) → hash each file → extract+validate manifest → look up previous approved version → compute file_summary and manifest_diff_summary → upload bundle to MinIO (idempotent on SHA) → insert publish_request row. Lazy-imports dbRead/dbWrite/newUlid so the pure helpers (extract, diff) are unit-testable without booting Prisma. - src/server/routers/blocks.router.ts — submitVersion mutation, withdrawPublishRequest mutation, listMyPublishRequests query. - src/server/services/blocks/__tests__/publish-request.service.test.ts — 19 deterministic tests covering computeFileDiff (add/remove/change + order independence), computeManifestDiff (first-version, scalars, deep-object hash, large-value summarisation), and extractBundleMetadata (valid bundle, missing manifest, empty, invalid JSON, deterministic hashes, sorted file list). Phase 3+ (deferred): mod review backend (approveRequest, rejectRequest, listPendingRequests) + /apps/review UI + /apps/submit redesign + /apps/my-submissions UI. Forgejo terminology purge in Phase 4. See claudedocs/app-blocks-w1-publish-request-flow-v0-handoff-2026-05-28.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 3 — mod review queue + approve/reject Closes the dev → mod → live loop for App Blocks publish requests. Mods hit /apps/review to see pending submissions, click into one to view the manifest + diff summary + file change counts, and approve or reject with a reason. On approve the platform pre-creates the OauthClient + app_blocks row (first version) and atomically commits the bundle to Forgejo in a single multi-file commit; the existing git-push webhook then takes over and fires the Tekton build chain. Backend: - forgejo.service.ts +commitFiles, +listRepoTree. commitFiles uses the Forgejo `/contents` multi-file endpoint so a full repo rewrite is one push event → one webhook fire → one build, not N. replaceAllFiles=true emits delete operations for repo files that aren't in the bundle so starter scaffolding doesn't linger after first-approve. - publish-request.service.ts +listPendingRequests, +approveRequest, +rejectRequest, +fetchAndExtractBundleFiles. approveRequest re-uses the auto-register OauthClient pattern from 5b304be53 (lifted from submitApp); first-version path also creates the Forgejo repo from the starter and sets up the push webhook. Pre-inserts app_blocks with status='approved' so the git-push handler (which does an UPDATE, not UPSERT) finds the row when the Forgejo commit fires. - publish-request.schema.ts +listPendingRequestsSchema +approveRequestSchema +rejectRequestSchema. Rejection reason 10-2000 chars (shown to dev verbatim on /apps/my-submissions). - blocks.router.ts wires three new mod-only procedures behind ctx.user.isModerator + enforceAppBlocksFlag. UI: - pages/apps/review.tsx — Mantine table of pending requests + Modal with manifest viewer, file-list diff, manifest-field diff (added/removed/changed with from/to values), approve / reject buttons. /apps/submit and /apps/installed terminology purge moves to Phase 4. Trust model: mods review the manifest + file list (counts + paths). Per-file source diffs deferred to Phase 4+; the build pipeline's container limits + CSP are the enforced sandbox. Phase 4 next: dev-facing /apps/submit redesign (drop the Forgejo / git-clone copy + the OauthClient picker shipped in 5b304be53), new /apps/[slug]/submit-version page, /apps/my-submissions page. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 4 — dev-facing UI redesign + Forgejo purge Rewrites /apps/submit for the ZIP-upload publish-request flow and adds /apps/my-submissions for the dev's view of their submission history. /apps/submit: - Drops the OauthClient picker + SegmentedControl shipped in 5b304be53 (auto-create now happens server-side in approveRequest, Phase 3). - Drops the "Forgejo repo created" / "git clone" / "Clone URL" copy shipped in W2 — devs never see Forgejo under W1. - Drops the form name + description fields (Phase 2 decision: trust the manifest, no duplicate truth). - New form: slug + version + ZIP upload (FileInput, accept=".zip", 50 MiB cap visualized). - Browser reads file as base64 via FileReader.readAsDataURL, posts via blocks.submitVersion. Success card surfaces the publishRequestId + links to /apps/my-submissions. /apps/my-submissions (new): - Table of viewer's publish requests (newest first, from blocks.listMyPublishRequests). - Inline rejection-reason row beneath any rejected submission (red background, whitespace: pre-wrap). - Inline approval-notes row beneath approved submissions (green). - Withdraw button on pending; Open-live on approved (target=_blank); Resubmit on rejected. - Fragments wrapped with key={s.id} so the conditional reject/approve rows don't break React's array reconciliation. Terminology audit: /apps/installed, /apps/index, /apps/[appBlockId] all clean — no Forgejo/repo/git-clone copy to remove. The old blocks.submitApp endpoint (with the 5b304be53 auto-register code) is left in place — no longer the canonical path but still callable. Phase 6 cleanup removes it once Phase 5 backfills the existing generate-from-model app into the new publish_request table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(blocks): drop unused Anchor import in my-submissions * feat(blocks): W1 v0 Phase 5 — backfillPublishRequest for live apps Migration helper that reconstructs a publish_request row for an app whose first version predates the W1 flow. Pulls the current Forgejo state into an in-memory ZIP, uploads to MinIO, and inserts a status='approved' row linked to the existing app_blocks entry. Without this, the first real submitVersion against the live generate-from-model app would diff its bundle against nothing and report "+all files" as the change set. forgejo.service.ts: - export listRepoTree (was internal to commitFiles) - +getBlobContent — reads a single blob via /repos/{owner}/{repo}/git/blobs/{sha}, decodes base64, returns Buffer. publish-request.service.ts: - +backfillPublishRequest. Pipeline: lookup app_blocks → fetch repo metadata → recursive tree walk → parallel blob downloads (8 in flight) → JSZip rebuild with epoch dates for deterministic bundleSha256 → upload to MinIO → reuse extractBundleMetadata for path/sha/size semantics consistent with live submissions → INSERT publish_request status='approved' with synthetic first-version file_summary and manifest_diff_summary. - Idempotent: re-running with the same Forgejo HEAD returns the existing publish_request via the (slug, bundleSha256) lookup; no duplicate row, no duplicate MinIO put (overwrites identical bytes). - Owner attribution: submittedByUserId comes from OauthClient.userId of the live app; reviewedByUserId is the mod invoking the backfill; reviewedAt = now. Schema + router: - backfillPublishRequestSchema (slug + optional approvalNotes). - blocks.backfillPublishRequest mutation, guarded by isModerator + appBlocks Flipt flag. Phase 6 deferred: Discord notify, queue-depth metrics, remove the legacy blocks.submitApp endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W1 v0 Phase 6 — Discord notify on new pending + remove legacy submitApp Discord: - publish-request.service.ts +notifyModsOfNewRequest. Posts an embed to DISCORD_WEBHOOK_MOD_ALERTS with slug, version, submitter, change summary (first-version vs +/~/- file counts), request ID, and a link to /apps/review. Fire-and-forget; 5s timeout; both inner fetch and outer wrapper catch — a Discord outage cannot block submitVersion. - submitVersion calls it via `void notifyModsOfNewRequest(...)` after the publish_request INSERT, with the submitter's username denormalized from a separate dbRead.user lookup. Legacy cleanup: - Removed blocks.submitApp procedure (~160 lines). Under W1 the developer-facing "create repo" step disappears entirely — OauthClient + Forgejo repo + app_blocks row are all created server-side in approveRequest (Phase 3) on first-version approve. The 5b304be53 auto-register SegmentedControl UI was already gone in Phase 4; this drops the now-orphan server endpoint. - Removed now-unused `newUlid` import from blocks.router.ts (every remaining use of newUlid lives inside the publish-request service). - Updated the dangling submitApp reference in git-push.ts:140 to describe the W1 pre-create-in-approve semantics. Skipped for v0: Prometheus metrics on queue depth + time-to-review. Mods can SQL-query app_block_publish_requests directly; revisit if the queue starts mod-fatiguing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(blocks): W1 v0 publish-request flow test coverage + audit follow-ups Adds vitest coverage for the W1 v0 publish-request orchestration layer (submitVersion, withdrawRequest, listPendingRequests, approveRequest, rejectRequest, backfillPublishRequest) plus the new forgejo.service helpers (listRepoTree, getBlobContent, commitFiles). 19 -> 82 tests on the App Blocks W1 surface. Boundary cases for the 50 MiB / 10 MiB / 2000-file caps. 13 regression tests labeled REGRESSION (C-1..C-4, H-1..H-4, M-4) lock in current behavior on findings from claudedocs/app-blocks-w1-v0-audit-2026-05-28.md so when the fixes land the assertions flip clearly. New test files: - src/server/services/blocks/__tests__/publish-request.orchestration.test.ts - src/server/services/blocks/__tests__/forgejo.service.test.ts Extended: - src/server/services/blocks/__tests__/publish-request.service.test.ts (+9 boundary + schema cap tests) No production code changed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): C-1 + C-2 from W1 v0 audit C-1: tRPC bodyParser was capped at 17mb, silently 413-ing bundles >~12 MiB even though the schema cap (MAX_BUNDLE_SIZE_BYTES) and the UI both advertise 50 MiB. Raise to 72mb so a 50 MiB ZIP base64-encoded inside the tRPC envelope fits. Acknowledged: this widens the cap for every tRPC route. The v1+ migration path is a dedicated /api/internal/blocks/ upload-bundle route that isolates the cap to the bundle path. C-2: approveRequest's OauthClient.id was `${ulid()}-app-block-<slug>` (non-deterministic), so any retry after a mid-flow failure (e.g. Forgejo 500 on createRepoFromTemplate) generated a *different* OauthClient.id and silently piled up orphans across retries. With slug-derived deterministic id (`appblk-<slug>`): - Retry's oauthClient.create hits the PK unique constraint (P2002); catch + findUnique recovers the existing row instead of inserting a second one. - Two concurrent first-version approves for the same slug now collide at the OauthClient PK rather than each succeeding with distinct ids (incidentally blunts C-3 — see test FIX (C-3)). - Wrap appBlock.create in the same P2002 catch + findFirst recovery so the recovery is symmetric. The recovered row gets a manifest refresh to converge on the new state. Tests: - publish-request.orchestration.test.ts: - First-version happy path now asserts the deterministic id (ocArg.id === 'appblk-hello'). - REGRESSION (C-3) replaced by FIX (C-3): models two concurrent approvers, second hits P2002 on both creates, falls through to findUnique/findFirst, ends with one OauthClient + one AppBlock. - New FIX (C-2): retry after Forgejo 503 in attempt 1 successfully completes in attempt 2 without orphan accumulation. - New FIX (C-2): non-P2002 errors on oauthClient.create are surfaced rather than silently swallowed by the catch. - mockDbRead gained an `oauthClient.findUnique` mock to back the new recovery lookup. - All 84 W1 tests pass (45 orchestration + 28 service + 11 forgejo). The 4 baseline failures elsewhere (1 buzz-attribution + 3 checkpoint) are pre-existing and unrelated, per the audit's note. C-1 fix sketch (Option A from audit) ships; C-2 fix sketch (Option C — deterministic id) ships. C-3 is blunted as a side effect but the audit's full C-3 fix (`@@unique([blockId])` migration or SELECT FOR UPDATE) is deferred — the catch-based recovery is sound for v0 internal-team submission volumes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): C-3 + C-4 from W1 v0 audit — DB uniqueness constraints Adds two DB-level constraints that close the read-then-write race windows the C-2 OauthClient.id determinism couldn't reach: C-3: ALTER TABLE app_blocks ADD CONSTRAINT app_blocks_block_id_unique UNIQUE (block_id); The existing (app_id, block_id) constraint doesn't protect because each approve mints a fresh app_id (now deterministic per C-2, but a bare (block_id) constraint is the belt-and-suspenders). BlockRegistry and the JWT issuer both assume one app per slug; this is the DB-layer enforcement of that invariant. C-4: CREATE UNIQUE INDEX app_block_publish_requests_one_pending_per_slug ON app_block_publish_requests (slug) WHERE status='pending'; Closes the window between submitVersion's "no pending request?" findFirst and its INSERT. Partial index lets approved / rejected / withdrawn rows accumulate without conflict. Pre-flight verified clean on cnpg-cluster-nvme0 prod 2026-05-28: app_blocks duplicate block_ids: 0 rows publish_requests duplicate pending slugs: 0 rows Migration 20260528210000_w1_uniqueness_constraints applied manually. Service changes: - publish-request.service.ts submitVersion: wrap the INSERT in try/catch P2002. On collision, surface a human-readable error ("already has a pending publish request (race window); withdraw the other or retry") matching the app-layer check's message. - approveRequest's existing P2002 catch from the C-2 fix already handles AppBlock.create collisions on (block_id) — no code change there. Prisma schema: - AppBlock model gains @@unique([blockId], map: "app_blocks_block_id_unique") alongside the existing (appId, blockId) constraint. - The partial unique index on publish_requests is raw SQL (Prisma doesn't model partial unique constraints first-class on this version); generated client doesn't need to know — the runtime P2002 catch handles it. Tests: - REGRESSION (C-4) replaced by FIX (C-4): second concurrent submitVersion now throws the human-readable error after the partial-index P2002. - New FIX (C-4): non-P2002 errors on the INSERT are surfaced (not silently swallowed by the catch). - New FIX (C-3): AppBlock.create P2002 (block_id collision) falls through to findFirst + update existing — covers the case where C-2's OauthClient layer is bypassed but C-3's DB constraint still protects. - All 86 W1 tests pass (47 orchestration + 28 service + 11 forgejo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): derive slug + version from manifest, drop redundant form fields The submit form had separate slug + version TextInputs whose only purpose was to be cross-checked against manifest.blockId / manifest. version. A typo in either field produced a confusing "manifest blockId (hello-world) does not match form slug (hello-world-block)" error, when the right fix was to delete the form fields and trust the manifest as the source of truth. Schema: - submitVersionSchema drops slug + version; accepts only bundleBase64. Service: - submitVersion derives slug from manifest.blockId, version from manifest.version, name from manifest.name. Each gets a shape check (SLUG_REGEX, SEMVER_REGEX, non-empty) that surfaces a human-readable error before any S3 / DB writes happen. - Removed the form-vs-manifest cross-check branch. - Approve flow + downstream (commitFiles, app_blocks, OauthClient appblk-<slug>) all read slug via the existing `request.slug` path from the publish_request row — no change there. UI: - /apps/submit: drop slug + version TextInputs. On file pick, parse the ZIP client-side via jszip, extract block.manifest.json, validate the same shape rules as the server. Surface a preview card listing slug, version, name, description, contentRating, slots. Submit only enables when the preview parses cleanly — failures show inline with a clear message before the user round-trips. Tests: - 2 orchestration tests rewritten: "blockId does not match slug" → "blockId is not a valid slug" (sends `NotALowercaseSlug`); "version does not match" → "version is not valid semver" (sends `not-semver`). - All other submitVersion call sites had `slug:` + `version:` lines stripped (the params are no longer accepted by the function). Bundle helper's manifest defaults to blockId=hello / version=0.1.0 so behavior is preserved. - All 86 W1 tests pass (47 orchestration + 28 service + 11 forgejo). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): push bundle to in-review Forgejo repo + link from /apps/review So mods can see the actual code, not just the manifest diff. On every submitVersion we ensure a per-slug repo exists in a new civitai-apps-review org and commit the bundle's files there with replaceAllFiles=true. The review modal gains a "View code in Forgejo" button that deep-links to the repo's tree view, where Forgejo's diff UI is what the user wanted in the first place. Design choices: - One repo per slug (civitai-apps-review/<slug>), overwritten on every submitVersion. Single submission visible at any time (the C-4 partial unique index already enforces one pending per slug). - Separate org from civitai-apps so the git-push webhook + Tekton don't fire on review pushes — the build chain still triggers only when approveRequest commits to the canonical civitai-apps/<slug>. - Push happens BEFORE the publish_request INSERT. If Forgejo is sick, the submission fails clean (bundle stays in MinIO, idempotent on SHA; no orphan DB row). - Repo creation is idempotent (422/409 = already exists, fine). ensureReviewRepo also creates the org on first call. forgejo.service.ts: - New FORGEJO_REVIEW_ORG = 'civitai-apps-review' constant. - New ensureReviewRepo(slug) — creates org (POST /api/v1/orgs) and per- slug repo (POST /api/v1/orgs/<org>/repos with auto_init=true) idempotently. - New exported reviewRepoUrl(slug) helper for the UI link. - listRepoTree + commitFiles gained an optional `org` parameter (defaults to FORGEJO_ORG so existing callers are unchanged). publish-request.service.ts: - submitVersion re-decodes the bundle in-memory after the MinIO PUT and pushes per-file contents to the review repo with replaceAllFiles=true. ~50ms overhead for a 50 MiB bundle on a modern node; cheaper than threading per-file contents through extractBundleMetadata's return. - listPendingRequests payload now includes reviewRepoUrl(slug) so the UI doesn't need to construct it. review.tsx: - PendingRequest gains reviewRepoUrl: string. - Modal renders a default-style "View code in Forgejo" button with IconCode + IconExternalLink, target=_blank. Tests: - mockForgejo gained ensureReviewRepo + reviewRepoUrl stubs. - All 86 W1 tests pass (47 orchestration + 28 service + 11 forgejo). Known follow-up: forgejo.civitai.com's oauth2-proxy currently requires GH `oauth` team membership, so mods clicking the link 403 until they're added to that team (or the gate is loosened to org-only). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): make civitai-apps-review repos public for anonymous mod browsing Mods get a "Login Failed: Unable to find a valid CSRF token" error on forgejo.civitai.com's Forgejo-side login form (orthogonal cookie / CSRF issue; the oauth2-proxy gate itself works — Tekton dashboards behind the same gate load fine). Without a Forgejo login session, the per-slug review repo I just added (private by default) is unreadable from /apps/review's deep-link. Flip `private: true` → `private: false` so review-repo file trees are anonymously browsable inside Forgejo. The security boundary stays oauth2-proxy: every request to forgejo.civitai.com goes through the GH `oauth` team gate first; only inside that boundary does Forgejo serve the public view. Acceptable for these throwaway-per-submit review snapshots. Canonical civitai-apps repos stay private (built-and-deployed code; the git protocol path is open at `*.git` for Tekton clone auth, but the HTML browse view still needs a Forgejo session). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): use browser-facing FORGEJO_PUBLIC_URL for review-repo link reviewRepoUrl() was using getBaseUrl(), which returns FORGEJO_BASE_URL — the cluster-internal forgejo-http.forgejo.svc.cluster.local:3000 endpoint civitai-web uses for its API and webhook calls (avoids the Cloudflare + oauth2-proxy round-trip). The /apps/review modal link needs the BROWSER-facing URL. Add a new FORGEJO_PUBLIC_URL env (default `https://forgejo.civitai.com`) and have reviewRepoUrl read from it. FORGEJO_BASE_URL stays unchanged for all the other Forgejo service-layer calls (createRepo, commitFiles, listRepoTree, getBlobContent, etc.) which still want the in-cluster endpoint. Default works for prod + PR previews without any env wiring change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): inline-detect & replace existing pending submission on /apps/submit Resubmitting a bundle while a pending request already existed for the same slug surfaced a raw server error in a toast — even though the dev's intent was clearly to supersede their own pending row. Pre-flight the conflict during preview so we can offer a "withdraw and resubmit" affordance instead of letting the user submit into a guaranteed failure. - New blocks.getMyPendingForSlug query (own-rows-only) — surfaces an existing pending pubreq for the previewed slug. - /apps/submit fires it once manifest parses; renders a yellow Alert with the pending version + submittedAt + id and morphs the submit button to "Withdraw and resubmit". handleSubmit calls withdrawPublishRequest then submitVersion; withdraw failure short-circuits with a clean toast. - Sharpened the server-side same-slug error: same-user wording suggests self-withdrawal; other-user wording no longer leaks the conflicting pubreq id (useless to a non-owner). - Tests: split the existing same-slug rejection test into same-user vs other-user (asserts id is NOT in the other-user message); 3 new tests for getMyPendingForSlug (null when none, returns own row, where-clause scopes to caller). 79/79 publish-request tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): /apps/review history tabs for approved + rejected publish requests Adds Approved + Rejected tabs to the moderator review page so mods can browse the publish-request history with the inline approvalNotes / rejectionReason and the reviewer attribution surfaced. Active tab is mirrored to ?tab= for deep-linking. The review modal grows a read-only mode (no approve/reject buttons) for history rows and surfaces the mod feedback in a coloured callout. Backend: two new service functions (listApprovedRequests, listRejectedRequests) mirroring listPendingRequests, two tRPC procs wired through the same isModerator + enforceAppBlocksFlag gates. Schemas reuse the existing listPendingRequests cursor shape. Mod-history coverage in the orchestration test suite verifies status filtering, reviewedAt-desc ordering, inline notes, and cursor pagination (63 tests passing). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W3 Phase 4 — manifest-driven settings in AppSettingsModal The install modal hardcoded two fields (buzz_budget_per_gen NumberInput, default_checkpoint_version_id picker) which made sense for one block but leak as a UX bug everywhere else: a viewer-identity block like who-am-i that has no generation surface still showed users a "Buzz budget per generation" control with no effect. Replace with a manifest-driven renderer reading block.manifest.settings through the same ManifestSettings type the server's validateBlockSettings already consumes. Fields render per the meta-schema's widget contract (number/string/boolean × text/textarea/select/toggle/resource_picker). Apps with no settings declaration get a clean modal with no "Block settings" divider. Apps that declare custom fields get those fields, no modal change required. - Pre-W3 manifests that omit per-field `scope` are coerced to 'publisher' for back-compat (gen-from-model is the prod case). - persistScope preserves unknown keys on existing subscription rows so legacy default_checkpoint_version_id values aren't dropped on resave — the platform's checkpoint resolution chain still consults them, and the right place to remove a field is the app's own manifest. - Mantine-native renderer (NumberInput / TextInput / Textarea / Select / Switch / picker Button) rather than the SDK's headless SettingsForm, which would clash with the modal's design language. Same widget contract; eventual W6 component pack will own the convergence. - Bonus copy fix: the modal Badge tracking subscription-target toggles read "No scopes selected" — conflated with manifest.scopes (JWT scopes). Renamed to "No targets selected" to keep the two concepts distinct in the UX. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): readable reviewer notes + structured manifest view + install counts Three loosely-related UX fixes the W1 v0 dogfooding surfaced: 1. /apps/my-submissions reviewer notes were unreadable on dark theme — the rejected/approved feedback rows used --mantine-color-(red|green)-0 as a row background but the text inherited the default body color, which is light on dark theme → light-on-light. Replace the colored Table.Tr backgrounds with embedded Mantine Alert components which handle theme contrast natively (variant="light"). Same fix applied to the review-modal reviewer-history card (same root cause). 2. Add install counts to my-submissions table. New "Installs" column surfaces two compact pills per approved row: • ModelBlockInstall rows (per-model placements, blue, IconBox) • BlockUserSubscription rows (publisher + viewer scopes, grape, IconUsers) Pending-first-version + withdrawn-first-version rows have no AppBlock (FK populated on approve) so the cell renders "—". Backend: listMyPublishRequests now selects appBlock._count and flattens it onto the row as modelInstallCount + userSubscriptionCount. 3. /apps/review modal now parses the manifest into a structured view instead of dumping JSON.stringify into a ScrollArea. The new ManifestView renders five labelled cards: • Identity — name, blockId, version, content rating badge, trust tier, render mode, description body • JWT scopes — chips with human description per scope; unknown scopes flagged red (would fail at token issuance) • Slot targets — slot ids with priority + requiredContext list, human description per known slot • Iframe — src as link, sandbox flags as individual badges (allow-same-origin / allow-top-navigation / allow-popups-to-escape-sandbox flagged as higher-risk via orange + IconShieldLock + tooltip), dimensions, resizable • Settings — declared fields with type/widget badge, scope chip, requires_scope chip, label, description, default, range Anything outside the handled key set falls into an "Other manifest fields" Accordion with raw JSON so reviewers can still see unexpected payloads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): static manifest checks + apply-Job smoke test (catch broken apps before live) Two prevention layers for the class of bug that produced today's gen-from-model mixed-content incident: 1. Static iframe.src validation in submitVersion. Rejects at submit time (before mod review, before build, before deploy): - non-string / missing iframe.src - http: scheme (the obvious mixed-content trip) - hostname != "<blockId>.<APPS_DOMAIN>" — catches leftover hackathon URLs like https://blocks-pr2319.civitaic.com/<slug>/ - non-"/" pathname — catches the exact stale bundler-base + nginx redirect pattern that bit gen-from-model. Errors are human- readable and surface in /apps/submit as plain BAD_REQUEST. Tests: 7 new cases covering each rejection plus the canonical accept path. 2. Pre-flight smoke test inside the apply Job. Before kubectl apply touches the live Deployment: - kubectl run a Pod with the candidate image, runAsNonRoot+drop ALL+ RuntimeDefault (matches the namespace's PodSecurity:restricted) - wait for Ready (60s budget) - curl /healthz from the apply pod's container — must return 200 - curl / with one redirect hop — must return 200 + text/html - reject if any Location header in the chain embeds the in-pod port (:8080) — this is the precise signal that bit gen-from-model (nginx's redirect emitted $server_port, which Traefik proxied to the browser as http://<slug>.civit.ai:8080/, mixed-content-block) - EXIT trap cleans up the smoke pod even on script failure - Failure exits non-zero so the Job is marked Failed and the live Deployment is left untouched. Build chain surfaces the failure back through the existing job-watch. The script body is exported as buildApplyScript(ns) so we can pin its shape with orchestration tests in the future without restructuring the inline-args glue. RBAC follow-up (datapacket-talos commit, separate): apps-applier Role gains pods/log get verb so the smoke step can dump logs on Ready timeout. * fix(blocks): H-4 — validate manifest at approve time + Discord notify on webhook failure Two layered fixes for the class of "approved but never built" silent failure that produced 2026-05-29's gen-from-model incident: 1. H-4 fix (primary). approveRequest now runs the same BlockManifestValidator the git-push webhook runs, BEFORE any DB writes or the Forgejo commit. Without this fix the order was: a) approveRequest updates app_blocks.manifest in-place b) commitFiles fires the Forgejo webhook c) webhook 400s on the validator (e.g. sandbox flag "allow-popups-to-escape-sandbox" not allowed under trustTier=unverified) d) publish_request gets marked status='approved' anyway e) build chain silently never runs f) app_blocks row points at a manifest the live pod never serves With the fix, approveRequest rejects with a clear error that surfaces inline in /apps/review. The Forgejo commit, the four external system writes, and the publish_request status flip are all skipped. For first-version approves the OauthClient doesn't exist yet — approveRequest synthesises the AppContext it WOULD create (allowedScopes = Prisma schema default 33554431; allowedOrigins = [https://<slug>.<APPS_DOMAIN>]) so the validator runs on the same shape the webhook will see seconds later. For subsequent versions it reads the existing OauthClient via the AppBlock.app relation (existingAppBlock query extended to select the two fields). Two new orchestration tests assert the rejection wiring: - subsequent-version reject: no AppBlock update, no Forgejo commit, no S3 read, no publish_request update. - first-version reject: no OauthClient create, no Forgejo repo create, no webhook setup, no AppBlock create, no commit, no publish_request update. The previous REGRESSION (H-4) test (asserting the broken legacy behavior) is flipped into a FIX (H-4) test that asserts the new rejection path. Default test fixture grew contentRating + scopes + iframe.{maxHeight, resizable, sandbox} so existing happy-path tests still pass the stricter validator. 71/71 publish-request orchestration tests green. 2. Defense in depth: Discord notify on every webhook failure path. git-push.ts gains notifyModsOfWebhookFailure, wired into all five failure paths (fetch-manifest, parse-manifest, manifest-validation, blockId-slug-mismatch, iframe-src-mismatch, trigger-build). After H-4 this should fire only on direct Forgejo pushes that bypassed the approve flow, or if the approve-side and webhook-side validators ever drift. Fire-and-forget, 5s timeout, no-op when DISCORD_WEBHOOK_MOD_ALERTS is unset. Same payload shape as the existing notifyModsOfNewRequest helper. * fix(blocks): smoke-test pod needs imagePullSecrets for private ghcr images First real run of the pre-flight smoke step in the apply Job (2026-05-30 13:14 UTC, gen-from-model v0.2.1) sat in ImagePullBackOff: failed to pull and unpack image "ghcr.io/civitai/app-block-generate-from-model:81b5fe3b...": failed to authorize: 401 Unauthorized Block-app images are pushed to private ghcr repos. The main Deployment template propagates imagePullSecrets: [ghcr-cred] from the per-app manifest, but my `kubectl run --overrides` for the smoke pod didn't include it — the override JSON only set securityContext + automountServiceAccountToken + container shape. Add imagePullSecrets: [{name: ghcr-cred}] to the pod spec overrides. The ghcr-cred Secret already exists in civitai-apps (referenced by every per-app Deployment) so this is a one-line fix; no new RBAC, no new Secret. The stuck v0.2.1 apply was manually unblocked by deleting the failing Job + smoke pod and setting deploy/generate-from-model's image directly to the Tekton-built tag. Subsequent builds will exercise this fixed smoke step end-to-end. * feat(blocks): W8 multi-install tabs in BlockSlotClient Multi-install slots now render as Mantine Tabs ordered by manifest priority desc (per slot) then name asc. Single-install path is unchanged. Only the active tab's BlockHost is mounted at a time — inactive installs don't issue JWT tokens (cost + audit noise). Ordering logic extracted to a pure sortInstallsForSlot helper with 19 unit tests covering priority fallbacks, slot-specific priority lookup, name tiebreaker, malformed-target defense, and immutability. * feat(blocks): W5 v0 scope-reflection + activity feed on /apps/installed Adds two read-only reflection surfaces so users can see (a) what each installed app can request and (b) what apps have actually done on their behalf. v0 is deliberately a reflection layer, not a consent layer — the W5 grant schema is v1 work. Backend: two new tRPC queries on blocksRouter, both flag-gated like the rest of the file. - listMyScopeGrants aggregates per-app from enabled model_block_installs + block_user_subscriptions (one row per AppBlock, dedup'd). - listMyAppActivity is a cursor-paginated walk of block_buzz_attribution filtered by userId = ctx.user.id, ordered by attributedAt desc + id tiebreak. Cap 100, limit+1 trailing-row pagination pattern. Frontend: extends /apps/installed with a Mantine Tabs section ('Subscriptions' / 'Apps & permissions' / 'Recent activity'). Refactor: SCOPE_DESCRIPTIONS + SLOT_DESCRIPTIONS extracted from review.tsx to ~/server/services/blocks/scope-descriptions.constants so both pages share the same friendly-description source of truth. 23 orchestration tests for the service (aggregation, dedup, sort, scope fallback, cursor pagination, limit cap, user filtering). * feat(blocks): W5 v0.5 — version pin, uninstall, scope audit log on /apps/installed Adds the three /apps/installed extensions: 1. Per-install version pin (model_block_installs.pinned_version). NULL = follow latest approved release; a semver string = stored preference for that version's manifest. Wired so W2-v1 multi-version hosting can route on this column without another migration. tRPC: blocks.setInstallPinnedVersion validates ownership + approval status. 2. Uninstall button (UI). The existing blocks.uninstallFromModel tRPC already covers the data path; the Model installs tab adds the confirm-modal + invalidates listForModel for the affected modelId. 3. Scope-invocation audit log (block_scope_invocations). One row per scope-gated API call, written from block-scope.middleware.ts on res.on('finish'). Fire-and-forget — never blocks the response. /apps/installed Activity tab now interleaves Buzz attribution + scope invocations on a single timeline. UI: - New "Model installs" tab between Subscriptions and Apps & permissions. Each row: app + model link + slot + version Select + uninstall icon. Version dropdown shows "Latest (<currentVersion>)" + every approved version newest-first. - Activity tab gains an interleaved feed; status badge renders 2xx green, 3xx blue, 4xx orange, 5xx red for scope rows. Backend: - ALTER TABLE model_block_installs ADD COLUMN pinned_version TEXT. - CREATE TABLE block_scope_invocations + (user_id, invoked_at DESC, id DESC) index for the per-user feed + (app_block_id, invoked_at DESC) for the future per-app drill-down. - SignBlockTokenInput gains appBlockId so middleware logs don't need a per-request DB lookup; claim is required in BlockTokenClaims (strict shape check at verify time). Issuance passes block.id (the apb_<ulid>). Tests: +19 orchestration tests on user-app-surface (listMyModelInstalls batch-version-lookup, sort, ownership check, cursor coercion, BigInt→string serialisation, db-error swallowing). Existing block-token.service.test + block-scope.middleware.test updated to pass the new appBlockId field. Migration applied manually to prod cnpg-cluster-nvme0 per gotcha #14; backwards-compatible (additive column + new table, no data backfill). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): W11 dynamic origin allowlist + gotcha #39 apply-wait fix Two unrelated-looking changes that ship together because they're both load-bearing followups from the W5 v0.5 session. W11 — dynamic CORS allowlist from OauthClient.allowedOrigins: block-scope.middleware.ts's allowlist is now the UNION of (a) BLOCK_ALLOWED_ORIGINS env CSV (kept as a transition shim), and (b) every approved OauthClient row's allowedOrigins[] column — populated automatically by the W1 approve handler. In-memory cache, 60s TTL, single-flight refresh on miss. Source becomes async (originAllowed + setBlockCors return Promise<>); withBlockScope already async so the only behavior shift is sub-millisecond cache hits in steady state + ~one DB round trip per pod per minute. Eliminates the per-new-block 3-yaml SOPS edit + rollout dance that gated every new block subdomain. The OauthClient row is the canonical source; the env CSV exists only for pre-W1 hackathon-era rows. Dynamic-imports dbRead inside loadAllowedOrigins so this module stays load-time side-effect-free (lets test envs import it without a full Prisma init). Same trick applied to the recordScopeInvocation call from the W5 v0.5 logging path — the eager import was dragging user-app-surface.service into module init. Gotcha #39 — defer current_version_deployed_at write to apply success: Before: build-callback set app_blocks.current_version_deployed_at the moment triggerApply returned (i.e. as soon as the Job was created, BEFORE the smoke step + kubectl apply + rollout-status). A failed apply (smoke/perms/NP — see today's v0.2.2/0.2.3/0.2.4 trap chain) left the column saying "deployed at <now>" while the live Deployment sat on the previous image. Now: build-callback responds 200 to Tekton immediately (the build's handoff to apply is its job; Tekton doesn't care about apply outcome), then a fire-and-forget watcher polls the apply Job (6 min ceiling, 5s ticks) until Succeeded / Failed / timeout. Only on Succeeded does the column flip + commit status go green; on Failed/timeout the column keeps its previous value (correctly reflecting the LAST successful deploy) and commit status flips red. New helper waitForApplyJob in apps-pipeline.service.ts polls the Job via .status.succeeded + Failed conditions (avoid .status.failed which counts attempts mid-backoff). Reusable for future per-app rollout status surfaces. Pod restart loses the watch handle — the column self-heals on the next successful build. Acceptable for v0; v1 polish would persist the watch via a CronJob reconciler or a Job-finalizer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(apps): add "My installed apps" link to /apps header Closes the one-way navigation gap: /apps/installed already links to /apps ("Browse the marketplace"), but /apps had no reverse path back to where users manage what they've subscribed to / installed. After a user subscribes from the marketplace they had no in-page affordance for "where do I see this now?" Always rendered (not gated like the mod-only SubmitAppLink) since the target page handles anonymous → /login redirect itself. Icon matches the Subscriptions tab on /apps/installed (IconPlugConnected) for visual consistency. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ux(blocks): clarify Subscriptions vs Per-model installs on /apps/installed A user with multiple subscriptions but no per-model-pinned installs hit the Model installs tab and saw it empty, then assumed it was buggy. The tab name and empty-state copy didn't distinguish between two distinct install paths in App Blocks: - Subscriptions (block_user_subscriptions): "this app on ALL my models" / "on EVERY model page I visit" — covered by the existing Subscriptions tab. - Per-model installs (model_block_installs): "this app pinned to THIS specific model" — the affordance for one-off, model-specific placement. Changes: - Rename tab label "Model installs" → "Per-model installs" so it's clear from the chip alone that this isn't where Subscriptions live. - Header copy explicitly contrasts the two paths and points to Subscriptions for the blanket case. - Empty-state copy bridges back to the Subscriptions tab so a user who subscribed (and assumed they "installed") finds where their rows actually are. - Activity tab empty-state also gets a real explanation: it only populates on Buzz purchase from inside a block (openPurchaseModal attribution) or scope-gated REST API calls — NOT for vanilla generations that spend existing balance. Stops the "I generated stuff, why is this empty?" confusion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(blocks): kill per-model installs — absorb into block_user_subscriptions Deprecates `model_block_installs` as a user-facing concept and folds the per-model install primitive into `block_user_subscriptions`. One install surface instead of two; the data model now expresses pinning as a subscription with slot_id + target_model_ids[] populated. Why: - Real users don't distinguish "pinned to one model" from "blanket on all my models" — both are "install this app." Carrying two surfaces meant two tRPC procs, two /apps/installed tabs, two mental models. - block_user_subscriptions already had target_model_types[] + target_base_models[]; target_model_ids[] is the natural extension that lets it cover the per-model case too. The three filters AND together at listForModel time. Schema (migration 20260530210000_kill_per_model_installs, applied to prod cnpg-cluster-nvme0): - block_user_subscriptions: ADD target_model_ids INT[], slot_id TEXT, pinned_version TEXT, block_instance_id TEXT UNIQUE, installed_by _user_id INT. - Drop the (user_id, app_block_id, scope) UNIQUE; replace with two partial unique indexes — one for blanket subs, one for pinned subs. - block_user_settings FK repointed from model_block_installs to block_user_subscriptions (the bki_* id is preserved across migration for the one row that existed in prod). - DROP TABLE model_block_installs CASCADE. Code: - BlockRegistry.listForModel: rank-1 SQL branch is now the pinned-sub shape (slot_id non-NULL + target_model_ids contains modelId). Rank-2 is the blanket publisher-sub shape with slot_id IS NULL + empty target_model_ids. NOT EXISTS suppression checks the pinned-sub shape regardless of enabled — preserves publisher opt-out semantics. - BlockRegistry.resolveBlockInstance: mbi_*/bki_* prefix now looks up in block_user_subscriptions via the preserved block_instance_id column. Adds defense-in-depth Model.userId === bus.userId check. - BlockRegistry.installOnModel / uninstallFromModel / toggleEnabled / updateSettings: rewritten to operate on the pinned subscription shape. installOnModel uses findFirst+create/update (Prisma can't express the partial UNIQUE inline). - BlockRegistry.upsertSubscription: kept as the BLANKET-only write path. Pinning goes through installOnModel. - BlockRegistry.listUserSubscriptions: returns new fields (targetModel Ids, slotId, pinnedVersion, blockInstanceId, currentVersion, available Versions, pinnedModelNames) so /apps/installed can render version selector + uninstall + "Pinned to: <ModelName>" badges off one query. tRPC: - DROP blocks.listMyModelInstalls and blocks.setInstallPinnedVersion. - ADD blocks.setSubscriptionPinnedVersion (keyed on subscription id). - installOnModel / uninstallFromModel / toggleEnabled wire shape unchanged — iframe SDK callers (block-tokens, IframeHost) keep working transparently. - blocks.listMyPublishRequests: install count comes from a groupBy on pinned subscriptions (was the _count relation on the removed table). UI: - /apps/installed: drop the "Per-model installs" tab + ModelInstallsPanel. SubscriptionRow now handles both shapes — renders pinnedModelNames as small "Pinned to: <ModelName>" badges, exposes version Select + uninstall button when isPinned. Toggle uses toggleEnabled for pinned subs (preserving rank-1 NOT EXISTS suppression) and upsertSubscription for blanket. Downstream: - workflow-completed.ts + testing/blocks.ts: swap modelBlockInstall .findUnique → blockUserSubscription.findUnique by blockInstanceId. - PublisherSubscriptionBanner: unchanged. Its opt-out path (install + toggle disable) still works because installOnModel now creates a pinned subscription that suppresses the blanket at rank-1. After migration: the single live row (mbi_01KSD3NP23EQHXEPQRH32EX72G on model 2522512) became bus_pin_01KSD3NP23EQHXEPQRH32EX72G with its original bki_01KSD3NP23DEQQN4T264GFN3RH preserved. The block on generate-from-model.civit.ai's sidebar continues to resolve through the same blockInstanceId — block_buzz_attribution + block_user_ settings rows keep their referent. Tests: 102/102 in the directly-modified files (block-registry × 4, user-app-surface.orchestration). The previously-failing M2 install settings test in block-registry.service.test.ts now passes (replaced the modelBlockInstall.upsert mock with the new findFirst+update path). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): kill_per_model_installs typecheck — mock signatures + listMyPublishRequests inference Two follow-ups to f3890bbd2 ("kill per-model installs") to clear the new tsc errors Tekton's pr-preview would catch: - block-registry.service.test.ts: vi.fn(async () => []) infers the return type as Promise<never[]>, which then rejects every .mockResolvedValue([{appBlockId: 'ab_one'}, ...]) call site with "Type {appBlockId: string} is not assignable to type never". Widened each hoisted mock to an explicit `(..._a: unknown[]) => Promise<unknown|unknown[]>` signature so mock.calls + mockResolvedValue both type cleanly. - blocks.router.ts listMyPublishRequests: rows.map((r) => r.appBlock?.id) had implicit `any` on `r` after the agent's flatten extraction. Bound RawRow via `(typeof rows)[number]` + typed the filter narrowing. No behavior change. 18/18 block-registry.service tests + 42/42 user-app- surface tests + 9/10 block-scope middleware tests pass (the 1 failure is the pre-existing baseline path bug at src/server/middleware/__tests__/block-scope.middleware.test.ts:113 — unrelated to this commit chain). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): log workflow submissions to Activity feed The Activity tab was populated only by: - block_buzz_attribution rows (Buzz PURCHASES from inside a block via openPurchaseModal — publisher revenue share) - block_scope_invocations rows (scope-gated REST calls via the JWT bearer middleware) Vanilla generations spending existing Buzz balance hit NEITHER path, so a user who ran "Generate" 10 times saw an empty Activity tab and reasonably wondered if it was buggy. Fix: piggyback on the existing block_scope_invocations table. After blocks.submitWorkflow's orchestrator call returns, fire-and-forget a recordScopeInvocation row with scope='ai:write:budgeted' + a synthetic endpoint='workflow:submit:<workflowId>' (the path is tRPC, not REST, so the endpoint string is synthetic). statusCode maps from snapshot.status to 200/500 so the existing color-coded badge keeps working. UI: humaniseScopeInvocation special-cases the workflow:submit prefix to render "Generated an image" instead of the generic "Submit AI workflow" scope label. The Detail column strips the synthetic prefix to show just the workflowId. Empty-state copy updated since vanilla generations now DO populate the feed. No schema change — reuses the existing block_scope_invocations table. Historical generations are not backfilled (no source data). Future generations from this commit forward will appear in the Activity feed ~immediately after the orchestrator submit returns. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(blocks): audit hooks on remaining bridge-callable mutations + W4 unify After a54d56aff covered submitWorkflow, the Activity feed still missed: - updateUserSettings (viewer settings writes from the bridge, incl. SET_CHECKPOINT pin swaps) - apps.storage.set (W4 KV write) - apps.storage.delete (W4 KV delete) Each gap = an app action the user couldn't see in their audit trail. W4 already had a per-write logToAxiom call, but that surface is ops-only — not visible on /apps/installed. Fix: same shape as the workflow fix. Each mutation calls recordScopeInvocation post-success with a synthetic endpoint string: - user-settings:write (block:settings:write scope) - storage:set:<key> (apps:storage scope, new) - storage:delete:<key> (apps:storage scope, new, only on actual deletion — no-op deletes shouldn't pollute the feed) UI: - humaniseScopeInvocation: special-cased verbs for each pattern ("Saved your block settings", "Wrote app-local storage", "Deleted app-local storage") - humaniseScopeEndpoint: strips synthetic prefixes — workflow id, storage key, etc. surface clean in the Detail column W4 unify: NO new table — the existing logToAxiom call stays (ops/debug visibility) and the new recordScopeInvocation call populates the user-facing audit feed. Single SOT per surface, no double-bookkeeping. Out of scope (intentional): - estimateWorkflow / pollWorkflow — noisy (runs every page load / every 2s during a generation). Adds no signal. - install / uninstall / subscribe / unsubscribe / toggleEnabled — these are USER actions from platform UI, not actions apps take on the user's behalf. They don't belong in this feed. - OPEN_BUZZ_PURCHASE "ask" event — postMessage is client-side only; the COMPLETED buy already writes block_buzz_attribution. The "ask" without a completion is marginal value. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(blocks): escape */ in BlockUserSubscription doc comment breaking prisma generate The blockInstanceId doc comment ended in `bus_pub_*/bus_view_*`. Prisma emits /// comments as JSDoc /** */ blocks in the generated client index.d.ts, so the `*/` closed the comment early and spilled text into TS, producing a corrupt .d.ts (TS1161 unterminated regex literal). This failed the Tekton typecheck on every commit since f3890bbd2 (kill_per_model_installs), so no deployable image built — leaving PR-2319 on the pre-migration f0dfd2980 image against a prod DB that had already dropped model_block_installs. Add a space (`bus_pub_* / bus_view_*`) to break the */ adjacency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview build (prior run hit buildkit lock contention) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(blocks): W7 host-rendered trust frame around app blocks (IframeHost) App blocks now render inside a host-controlled frame: a bordered container with a top chrome bar (Civitai "App block" badge + a menu whose "Manage apps" item links to /apps/installed). Rendered in civitai-web AROUND the iframe, NOT inside it — so a sandboxed third-party block can't fake, restyle, or hide the signal. This is the safety affordance that lets users distinguish app blocks from native Civitai UI. Always present during loading + ready. Mantine 7 (Group/Menu/ActionIcon), NextLink for the route. Per-file LSP typecheck clean; AppBlocks vitest green. Note: an earlier attempt put this inside the generate-from-model iframe (block v0.2.7) — wrong layer (spoofable). That in-iframe bar is being removed; this host frame supersedes it. * feat(blocks): unify /apps/installed into one-row-per-app list (surfaces as per-install setting) Collapse the Installs tab's two scope-split sections ("On models I own" / "On model pages I view") into a single list with one card per installed app. Blanket publisher/viewer subs become a "Shows on" badge summary (each with a location+audience tooltip); pinned per-model installs render in a subsection with the version Select + Uninstall controls preserved. Both-surface toggling still goes through the existing AppSettingsModal via Manage. Adds groupSubscriptionsByApp() pure helper + node-env vitest coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (sharp native-install flake, not code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): move groupSubscriptionsByApp helper+test out of src/pages/ The helper + its vitest file were under src/pages/apps/, so Next.js (default pageExtensions) tried to compile them as routes — webpack then followed the test's `vitest` import and failed on `node:module`, breaking the production build (typecheck passed; build-image failed). Moved both to src/components/Apps/ (next to AppSettingsModal) and repointed the import. No logic change; 6 grouping tests still green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger (sharp/libvips native-install flake on build node) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): trust tier is moderator-controlled, not publisher-self-declared (C1) approveRequest read `trustTier` verbatim from the publisher manifest and defaulted a missing value to `internal` — the MOST privileged tier, which grants `allow-same-origin` (sandbox escape). A third-party manifest could self-escalate by declaring `"trustTier":"internal"` or omitting it. Fix: resolvedTrustTier = existingAppBlock?.trustTier ?? 'unverified'. The manifest's trustTier is normalised to the resolved value before the BlockManifestValidator call (it reads manifest.trustTier to gate the sandbox allowlist) and persisted at all 3 approve sites. Trust tier is now only raisable by a deliberate out-of-band moderator/DB action on the trust_tier column — never a manifest field. New apps default `unverified`; existing apps keep their current tier on re-approve (the 3 live internal blocks are all first-party and unaffected). Runtime sandbox reads the trust_tier column, so this closes the runtime self-escalation. approveRequest is DB/MinIO/Forgejo-coupled and has no unit harness (only the pure extract/diff helpers are tested); change is verified by typecheck. Part of the 2026-05-31 design-gap scan (C1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): marketplace install_count = distinct users, not subscription rows (M3) listAvailable counted block_user_subscriptions rows per app, so one user holding several rows for an app (blanket publisher + blanket viewer + N pinned-to-model subs after kill_per_model_installs) inflated the count — a pin-happy publisher could rank their own app higher. COUNT(DISTINCT user_id) makes install_count mean "distinct users." Marketplace ranking/display only; does not touch the listForModel rendering path. Part of the 2026-05-31 design-gap scan (M3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (ghcr push network timeout, not code) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(blocks): behavioral harness for listForModel — real SQL on PGlite Adds an in-process Postgres (PGlite, Postgres-in-WASM) harness that executes the unmodified listForModel UNION-ALL query, so the install-model resolution bugs (H2, H2b) can be driven test-first. The existing block-registry test only asserts on the SQL string shape and mocks $queryRaw to return [] — it cannot catch behavioral bugs. - @electric-sql/pglite devDependency (executes @>, = ANY, cardinality, array_length — all PG-only operators the query uses). - listForModel.harness.ts: PGlite-backed $queryRaw bridge + schema + seed helpers (only the columns the query reads). - listForModel.behavior.test.ts: 12 green precedence/happy-path tests locking current correct behavior, plus 3 it.fails tripwires documenting the bugs. Empirical findings (real query run): - H2 REPRODUCES (rank-2 blanket + rank-3 default): a type-filtered pinned row that does NOT apply to the model still suppresses the fallback → blank slot. The NOT EXISTS suppressors match on (scope, slot, app_block, modelId) but do not re-check the pinned row's own target_model_types/target_base_models. - H2b REPRODUCES only in the SAME-app shape: an x-rated pin survives the SQL but is dropped by the JS content-rating filter, while the same-app platform default it suppressed in SQL is already gone → empty slot. A DIFFERENT-app fallback is NOT affected (suppressor is keyed on app_block_id) — kept as a green control test. Service code unchanged — fixes are a separate follow-up; flip each it.fails to it() once the suppressor re-checks pinned filters / rating. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): H2 — pinned-sub suppressor must honour the pin's own type/base filters listForModel's rank-2/3/4 NOT EXISTS suppressors (and the rank-1 pinned SELECT) matched a pinned subscription on (scope, slot_id, app_block_id, modelId ∈ target_model_ids) but never re-checked the pin's own target_model_types / target_base_models. So a pinned row whose filters EXCLUDE this model still suppressed the blanket sub + platform default → blank slot where the publisher's app should show. Latent today (installOnModel writes empty filters) but the schema permits filtered pins. Fix: apply the same type/base predicate the blanket subs use to the rank-1 pinned SELECT and all three suppressor subqueries, so a non-applicable pin neither renders nor suppresses. Proven against the new listForModel PGlite harness — the two H2 tripwires flip green; precedence + opt-out invariants unchanged. 44/44 across the block-registry suites. H2b (content-rating) re-analysed and confirmed NOT a bug: suppressors are keyed on app_block_id, so a content-dropped pin only blanks its own same-rated fallback — an empty slot is correct. Harness test corrected from an it.fails to a green assertion documenting that. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (sharp fix now live in npm-typecheck task) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): W7 frame wraps error/timeout/fatal states too (FRAME-1) The host trust frame (AppBlockChrome) was rendered only in the success branch; the timeout/fatal/no_token/bad-src early returns dropped it, so a block could shed the "App block" provenance chrome + the "Manage apps" escape hatch by never sending BLOCK_READY (→ timeout) or sending BLOCK_ERROR{fatal}. Route every state through a `framed()` helper so the host chrome is present whenever the slot is occupied, including failures. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): FIN-1 — server-side re-validate buzz revenue attribution App Blocks buzz revenue attribution was client-forgeable end-to-end. The browser stamps blockAppId/blockAppBlockId/blockInstanceId/blockScope/ blockModelId + metadata.userId into the Stripe PaymentIntent metadata; getPaymentIntent copied it verbatim to Stripe and the webhook credited a publisher's revenue share off those fields. The only control was an isSelfPurchase check, defeatable by a 2-account ring: any authed user could assert a confederate's app + viewer_personal (25%) scope and mint fake publisher earnings on a purchase that never touched a block. Add a server-side chokepoint in getPaymentIntent (the last place holding the authenticated tRPC session) that re-validates / re-derives every block-attribution field against ctx.user.id before the PaymentIntent reaches Stripe: - spender: force metadata.userId to the session user; reject on mismatch - install existence: resolve via BlockRegistry.resolveBlockInstance as the session user (viewerUserId = ctx.user.id, db='write'); null -> STRIP all block fields so the purchase proceeds as a normal un-attributed buzz buy (never hard-reject a real-money purchase over a bad attribution) - scope: re-derived from the resolved instance's source, not client input - app: overwritten with the resolved install's app_id / app_block_id Carry slotId through the attribution wire shape (client-supplied + untrusted) so the resolver — which needs (modelId, slotId) — can re-validate; a forged slot simply fails to resolve and is stripped. Non-block purchases are unchanged passthrough. 11 node-env vitest cases cover all four forge vectors + legit + non-block. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (sharp compile toolchain complete: +pkg-config) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(blocks): PAYOUT-1 safety substrate — hold gate, idempotent mint, refund clawback Makes the block_buzz_attribution financial state machine safe to switch on WITHOUT building real money disbursement (still leadership-gated, gotcha #26). Three invariants from the 2026-05-31 design-gap scan: 1. confirm-pending hold gate: replaces the blanket pending->confirmed updateMany with a per-owner velocity/volume circuit-breaker. Owners whose aging batch exceeds HOLD_VELOCITY_COUNT (200) or HOLD_VELOCITY_CENTS ($1,000) in a sweep are parked status='held' for manual review instead of auto-ripening. Idempotent (only touches status='pending'); disjoint held/confirm writes via notIn. 2. Idempotent payout mint: new block_attribution_payout ledger table with UNIQUE(app_owner_user_id, period_key) — a publisher is paid at most once per period. mintPayoutForOwner() inserts the ledger row + flips contributing confirmed rows to paid_out in one transaction; P2002 on the UNIQUE is an idempotent no-op. Net<=0 carries the debt forward (no mint, no flip). NOT wired into the bulk-payout cron — disbursement stays a logging stub. 3. Refund-after-payout clawback: voidAttributionsForPayment now writes a NEGATIVE carry-forward entry_type='clawback' row (status='confirmed') for each previously-paid_out row, so the payout aggregator nets the debt out of the publisher's next period. Clawbacks are written BEFORE the void so a mid-flight crash is crash-safe + idempotent (synthetic-key P2002 dedups repeat refunds) — voiding first would lose the debt on retry. Schema: +hold_reason/held_at/entry_type on block_buzz_attribution, the non-negativity CHECK scoped to entry_type='purchase' + a clawback-non-positive mirror CHECK (conservation CHECK unchanged: 0+0+(-X) = -X). Migration written, NOT applied — needs manual application to prod cnpg-nvme0 (gotcha #14). Tests: 38 passing across confirm-pending / buzz-attribution / rate-card suites (hold gate, mint idempotency + carry-forward, clawback ordering/dedup/ conservation). Also corrected a pre-existing stale v1->v2 rate-card assertion. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): annotate mintPayoutForOwner txn callback return type (PAYOUT-1 typecheck) The $transaction callback's three return literals inferred `minted: boolean` (no contextual type), so the inferred union didn't match the MintPayoutResult discriminated union → tsc TS2322 (Tekton pr-preview-s49kj typecheck red). Annotate the callback `: Promise<MintPayoutResult>` so each return is contextually typed and the `minted: true|false` discriminants are preserved. Tests unchanged (21 passing); Serena per-file diagnostics clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(blocks): drop "App block" wordmark from host chrome bar (keep icon) The AppBlockChrome bar showed an IconApps + an "App block" uppercase wordmark. The icon + the frame already signal provenance, so the text is redundant — remove the <Text> (and the now-unused Text import). Added aria-label="App block" to the icon so screen readers keep the provenance signal. The dropdown's Menu.Label + menu aria-label are unchanged. * fix(app-blocks): server-seed slot reservation to kill model-page CLS Two stacked layout-shift causes on model pages when the App Block slot loads: Source A — BlockSlotClient returned null while blocks.listForModel was in flight, so the slot was 0px then popped to full height once the frame mounted, shoving sidebar content down. Source B — IframeHost swapped a hidden (display:none) iframe for a shown one on BLOCK_READY, a second jump when content height != minHeight. Fix: - SSR-prefetch blocks.listForModel on the model page (least-invasive: one source of truth, no new query) so the client useQuery hydrates with isLoading already false — no 0px flash. Input matches useBlockSlot's exactly so the React Query cache keys line up. - Add pure computeSlotReservation + CHROME_BAR_PX (35px, derived from AppBlockChrome) in a client-safe slotReservation module, re-exported by block-registry.service as BlockRegistry.getSlotReservation (reuses listForModel verbatim — cached/indexed, no N+1). - BlockSlotClient reserves a minHeight placeholder during loading ONLY when reservedHeight > 0; zero-install pages still return null (no dead gap). - useBlockSlot keeps previous data across refetch so the reserve doesn't collapse mid-refetch. - IframeHost renders the iframe visible-but-non-interactive (pointerEvents none until ready) at minHeight, with the loading skeleton overlaid at the same minHeight — no hidden->shown swap; READY grows minHeight->content (one bounded change, not 0->content). Tests (node-env .test.ts, matching the repo's vitest-include convention): - slotReservation.test.ts: empty->{false,0}, single/multi iframe-> max(minHeight)+CHROME_BAR_PX, inline-only->{true,0}, default fallback, CHROME_BAR_PX pinned at 35. - block-registry.slot-reservation.test.ts: getSlotReservation reuses listForModel and folds correctly. No schema change (read-path only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(blocks): retrigger pr-preview (build node egress recovered) * chore(blocks): retrigger pr-preview * chore(blocks): retrigger pr-preview (build-image flake; typecheck was green) * feat(blocks): real server-side workflow cancel (blocks.cancelWorkflow + host handler) Adds a true orchestrator-side cancel for app blocks, mirroring pollWorkflow: - blocks.cancelWorkflow tRPC procedure — verifies the block JWT + ai:write:budgeted scope, cancels on the orchestrator with the VIEWER's token (so ownership is enforced orchestrator-side, 403/404 for non-owned workflows — same gate as poll), then re-reads + returns the canceled snapshot. - IframeHost CANCEL_WORKFLOW postMessage handler → blocks.cancelWorkflow, echoes WORKFLOW_CANCELED on the matching requestId (or a failure snapshot). Pairs with @civitai/app-sdk 0.7.0 (CANCEL_WORKFLOW/WORKFLOW_CANCELED messages) and @civitai/blocks-react 0.5.0 (useBuzzWorkflow().cancel). The host uses string- literal messages so it's decoupled from the SDK publish — a block that doesn't send CANCEL_WORKFLOW is unaffected. +4 router tests (cancel happy-path + the three auth gates). * fix(app-blocks): non-empty workflowId sentinel for whatif estimate snapshots The block SDK's inbound validator (isValidWorkflowSnapshot) drops any workflow snapshot whose workflowId is an empty string. A whatif/estimate call returns no orchestrator workflow id, so snapshotFromWorkflow emitted workflowId: '' — which the SDK silently dropped, stranding ESTIMATE_RESULT until the 120s transport timeout. The block then fell back to a '≤ budget' cost instead of the real estimate (reported as 'wrong buzz cost'). Emit a 'whatif' sentinel so estimate replies validate. The block treats estimate results as a cost quote only and correlates the reply by requestId (not workflowId), so a constant sentinel is safe. Submit always carries a real id and is unaffected. +1 regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): failure snapshots must use a non-empty workflowId (durable estimate-cost fix) ROOT CAUSE of the recurring 'CTA buzz cost never updates after estimateWorkflow' (reported 5x): the block SDK's inbound validator (isValidWorkflowSnapshot in @civitai/blocks-react) DROPS any workflow snapshot whose workflowId is an empty string. The host's failureSnapshot() — returned on EVERY estimate/submit/poll/ cancel error — used workflowId:''. So when blocks.estimateWorkflow threw on the host, the host DID post an ESTIMATE_RESULT error reply, but the SDK silently dropped it (console.warn only) → the block's pending request never resolved → it hung to the transport's 120s timeout → estimatedCost stayed null → the CTA sat on its '≤ budget' fallback. The real error was swallowed twice (validator + suppressed host stdout logging), which is why this was undiagnosable for 5 reports. My earlier #55 'fix' patched the SUCCESS path (snapshotFromWorkflow, which already gets a real whatif id) — a no-op. The empty workflowId was in the ERROR path all along. Fix: failureSnapshot now stamps workflowId:'failed' (extracted to its own module src/components/AppBlocks/failureSnapshot.ts with the invariant documented + unit tested). Same fix applied to the inline insufficient-budget snapshot in submitWorkflow (was workflowId:'' → would hang submit instead of showing the top-up CTA). The block side (separate commit) now surfaces a delivered failed snapshot as an estimate error instead of silently nulling the cost. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(app-blocks): retrigger pr-preview build (transient docker.io base-image TLS timeout on prior run, code typecheck was green) * feat(app-blocks): moderator-gate the entire feature (internal-only until GA) Phase 2 of the App Blocks graduation plan. The appBlocks feature flag (availability:['mod']) already hides the UI for non-mods, so the real gap was the API — close it with defense-in-depth so a direct tRPC/REST call leaks nothing even if a UI gate is bypassed. blocks.router.ts: - Convert the 23 management procedures from guardedProcedure (= any verified, not-muted user) to moderatorProcedure (= protectedProcedure .use(isMod)). guardedProcedure was NOT moderator — this was the crux gap. - For the publicProcedure block-token runtime procs (pollWorkflow, cancelWorkflow, estimateWorkflow, submitWorkflow, updateUserSettings) add an assertViewerIsModerator(userId) check on the token-RESOLVED viewer (not ctx.user). Factored into one shared helper to prevent drift. - For the session-authed reads: listForModel + listAvailable return empty for non-mods (graceful on user-facing pages); getShowcaseImages + getEffectiveCheckpoint throw FORBIDDEN. apps.router.ts (W4 KV storage): add the same assertViewerIsModerator on the resolved viewer inside the shared resolveStorageContext, covering get/set/delete/list/getQuota uniformly. block-token MINTING (api/v1/block-tokens): gate issuance on session.user.isModerator — the linchpin that makes the whole block-token runtime transitively mod-only. api/v1/blocks/me: re-assert resolved viewer isModerator (covers the ~15min window between a token mint and a demotion). Machine HMAC endpoints (api/internal/blocks/{git-push,build-callback, workflow-completed}) and the admin/webhook-token endpoints are deliberately left untouched — mod-gating them would break the Forgejo->Tekton->deploy chain and the orchestrator callback. UI: verified every /apps/* page + the model-page BlockSlot + all three nav links already gate on features.appBlocks (which is false for non-mods), and submit/review/my-submissions additionally gate on isModerator. No UI change was needed — they were already consistent. Tests: non-mod verified users now get FORBIDDEN from a sample of the formerly-guardedProcedure management procedures, from every block-token runtime proc, and from apps.storage; existing happy-path test contexts updated to moderator subjects. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(app-blocks): retrigger pr-preview build (transient corepack/npm-registry fetch flake on prior run — pnpm download failed before any typecheck ran; code unchanged) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(app-blocks): reliable pr-preview retrigger (empty retrigger commit d86e3d3 produced no webhook; prior run failed only at corepack pnpm-registry fetch, a transient infra flake — code is unchanged in substance) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * security(app-blocks): close OAuth account-takeover + scope-grant gaps (audit A1-A5,A7) Implements the confirmed-CRITICAL + selected-HIGH fixes from the App Blocks security audit (claudedocs/app-blocks-security-audit-2026-06-02.md §6). All provider-side changes are scoped to app-block clients ONLY (deterministic `appblk-<slug>` id prefix) so the legitimate OAuth-apps feature is unaffected. Fix 1 (A1 CRITICAL + A2/A3/A4 HIGH) — app-block OauthClients made structurally non-interactive + scope-capped: - block-scope.constants: add isAppBlockOauthClientId discriminator (migration- free, matches the `appblk-` id prefix; OAuth-apps use uuidv4 ids) + deriveOauthBitmaskFromBlockScopes. - publish-request.service: created OauthClient now gets grants:[] (removes the Prisma default authorization_code/refresh_token) and allowedScopes = the manifest-derived bitmask (NOT TokenScope.Full). Same ceiling fed to the approve-time validator. Subsequent-version + P2002-retry paths re-cap the existing client (self-heals pre-fix Full+grants rows). - authorize.ts + device.ts: reject `appblk-*` client_id with invalid_client. - oauth-client.router: refuse update/delete/rotateSecret on app-block clients. Fix 3 (A5 HIGH) — apps:storage made a declared/approved scope: - block-scope.constants: add apps:storage:read/write (SKIP_OAUTH_CHECK). - block-scope.middleware: matching enforceContextBinding cases (no fail-open). - apps.router resolveStorageContext: assert the read/write scope per op before touching appsDb. Fix 4 (A7 HIGH) — cumulative Buzz-spend cap: - blocks.router submitWorkflow: per-(user, app_block, UTC-day) Redis counter checked before submit, incremented after success; rejects when cumulative + cost would exceed the daily ceiling. Tests: +new vitest coverage for every fix (constants discriminator/bitmask, storage scope gate, buzz cap, OauthClient scope cap). Also repaired two pre-existing test-infra breaks surfaced while validating (missing appStorage* exports in the global prom mock; wrong import path in a middleware test). Local tsc clean on all touched files (remaining log noise is the codebase-wide Prisma type-gen artifact the GH Actions PR Check resolves via prisma generate). No schema migration required (prefix discriminator). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * security(app-blocks): per-user scope-grant consent (A6) + M-BUZZMODAL + M-POPUPS Phase 3b of the App Blocks graduation, on top of the A1 fix (67bdf60a9). A6 (audit HIGH / design-gaps C2) — close silent cross-version scope escalation: - New app_user_scope_grants(user_id, app_block_id, version, granted_scopes[], granted_at, revoked_at) table + Prisma model. MIGRATION WRITTEN, NOT APPLIED (hand-apply — gotcha #14): prisma/migrations/20260602120000_a6_app_user_scope_grants. - scope-grant.service: getGrantedScopes (fail-closed on missing/revoked), recordScopeGrant (additive, un-revokes, P2002-race-safe), partitionByConsent (consent-exempt: block:settings:*, apps:storage:*). - block-tokens mint intersects requested manifest scopes with the user's grant; withholds ungranted scopes, signs only the granted subset, returns needs_consent + missingScopes to the host. Builds beneath A1's manifest ceiling. - resolveBlockInstance now resolves the pinned version's manifest/approvedScopes from app_block_publish_requests when pinned_version is set (applyPinnedVersion), with fail-safe fallback to the live row. Applied to bki_/mbi_, bus_pub_, bus_view_ branches. - Grant lifecycle: installOnModel + upsertSubscription write the implicit first-consent grant (recordInstallConsent). A scope added in a later version routes through needs_consent until re-granted. - Minimal re-consent UX: BlockConsentPrompt surfaces needs_consent above the iframe; on accept calls blocks.grantScopes (server re-caps to manifest∩approved) then refreshes the token. useBlockToken threads needsConsent/missingScopes. M-BUZZMODAL: gate OPEN_BUZZ_PURCHASE on status==='ready' (BLOCK_READY received) via resolveBuzzPurchaseRequest; no-op before ready. M-POPUPS: drop allow-popups from the unverified sandbox tier (kept for verified/internal). Tests (23 new, all green): scope-grant.service (9), block-registry.pinned-version (4), openBuzzPurchaseGate / M-BUZZMODAL (4), M-POPUPS (2), block-tokens A6 scenarios (4: granted-A-only→needs_consent for B, grant B→A+B, revoked→withheld, consent-exempt signs). No new tsc errors on touched files; no test regressions (failing block-tokens/manifest-validator cases are pre-existing on 67bdf60a9). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): update stale tests to the mod-gated / A1 / H-8 / kill_per_model_installs behavior Greens the App Blocks vitest suite after Phase 2 mod-gating + audit fixes. - block-tokens/index.test.ts: token minting now requires isModerator (Phase 2, internal-only). Add isModerator:true to the success-path sessions; add an explicit non-mod-rejected-at-mint-gate test; make the ban/soft-delete cases mods so they exercise their own gate (not the upstream mod-gate). - block-manifest-validator.service.test.ts: H-8 added the allowedOrigins ceiling on iframe.src. Pass an AppContext whose allowedOrigins covers the manifest src for the success-path cases (the bare-number form defaults to [] and rejects). - checkpoint.service.test.ts: publisher install settings now resolve through BlockRegistry.resolveBlockInstance (model_block_installs was absorbed into block_user_subscriptions by kill_per_model_installs). Mock the resolver instead of the retired dbRead.modelBlockInstall.findUnique seam. - block-token.service.test.ts: provision the RSA keypair in test setup (the service reads env/server's import-time snapshot, so a beforeAll process.env set was too late) and verify with a KeyObject (jose v6 rejects a PEM Buffer). - setup.ts: wire a real BLOCK_TOKEN_{PRIVATE,PUBLIC}_KEY pair into the mocked env defaults; re-export the public PEM for the round-trip test. - prisma/models.ts: regenerated to include the App Blocks model interfaces the branch schema already defines. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): fix module-load crash in blocks.router.subscriptions test The suite crashed at import time (0 tests ran) because blocks.router imports getUserBuzzAccounts from buzz.service, which transitively loads redis/caches -> orchestrator/models -> resource-data.redis. That last module reads REDIS_KEYS.GENERATION.RESOURCE_DATA at module scope, which threw under the test's trimmed redis-client mock (no GENERATION key). Mock buzz.service at the boundary to cut the chain -- the same approach the sibling blocks.router.workflow.test.ts already uses. Also realign the two upsertSubscription settings-validation tests to the current manifest-driven validator (W3 validateBlockSettings) instead of the removed hardcoded per-blockId schema: the mocked appBlock now declares the buzz_budget_per_gen field on manifest.settings (publisher- and viewer-scoped respectively) so the range checks actually exercise the live code path. All 22 tests now run and pass, including the 8 Phase 2 non-mod -> FORBIDDEN security assertions that previously provided zero coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): harden cheap MEDIUM/LOW audit findings (M-WEBHOOK, L-CALLBACK, L-SANDBOX, L-DEDUP, L-M2, L-VERIFY/L-M6) App Blocks security audit (2026-06-02) §4 follow-up. Each fix is the smallest-correct change plus a vitest test. - M-WEBHOOK (git-push.ts): verify the push repo's org. The shared FORGEJO_WEBHOOK_SECRET authenticates the Forgejo *instance*, not a repo — the same instance also serves the civitai-apps-review org. Derive the slug from repository.full_name and require the canonical civitai-apps org (parseExpectedRepo), instead of trusting repository.name alone. - L-CALLBACK (build-callback.ts): bind the accepted imageRef to the callback's own slug + sha (expectedImageRef = ghcr.io/civitai/app-block- <slug>:<sha>). The bare app-block- prefix check let a signature-valid callback for slug A deploy app-block-<B>:<sha>, and accepted mutable :latest. - L-SANDBOX (sandbox.ts, extracted from IframeHost): intersectSandbox now fails closed to an explicit minimal safe set (allow-scripts) and unions declared+minimal, so it can never be wider than what the manifest declared; allow-same-origin stays tier-gated. - L-DEDUP (usePostMessage.ts): read the dedup requestId from payload (where the SDK transport puts it) via extractRequestId, not the always-undefined top-level data.requestId — replay dedup was inert. - L-M2 (attribution.schema.ts + attribution-validator.service.ts): align the attribution scope vocab post-kill_per_model_installs. mbi_*/bki_* are now per-model-PINNED publisher subscriptions, so both deriveScopeFromInstanceId and SOURCE_TO_SCOPE.install map them to publisher_all_my_models (same V2 publisher rate — no payout change, one bucket). per_model_install kept in the enum/rate-card for historical rows. - L-VERIFY / L-M6 (block-scope.middleware.ts): fail closed. verifyBlockToken now requires a kid and verifies against exactly that key (no fan-out to all keys); isBlockJwt requires typ=JWT exactly; the enforceContextBinding switch gets a default-deny. Verified safe: BlockTokenService.sign has stamped kid + typ:JWT on every token since the first App Blocks commit (5bf6f05b6), tokens live 15m and re-mint each render — no kid-less issuance era exists. Net test delta: 0 new failures (the one storage-provision.service.test.ts failure is pre-existing on the clean tip). Touched files are tsc-clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): adapt to React Query v5 API after main merge main's merge brought a React Query v4->v5 upgrade that broke the App Blocks UI code the Type Check caught (12 errors): - query option `keepPreviousData: true` removed -> `placeholderData: keepPreviousData` (import from @tanstack/react-query) in useBlockSlot.ts - mutation result `.isLoading` renamed to `.isPending` (AppSettingsModal, PublisherSubscriptionBanner, my-submissions, review, submit). Query `.isLoading` left as-is (still valid in v5). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): A8/BUILD-1 Phase 2 — drop tenant Dockerfile/nginx from build-source commit The build pipeline injects its own platform-owned Dockerfile + nginx.conf and ignores any tenant-supplied copies (gpu-fleet-infra #21). Committing the tenant copies to the canonical Forgejo repo (civitai-apps/<slug>, which the build clones) is therefore inert + misleading. Filter platform-owned paths (Dockerfile, nginx.conf, case-insensitive, repo-root) out of the approve commit. The in-review snapshot (civitai-apps-review) + the diff summary keep the full upload so mods still see exactly what the dev sent. Updated the orchestration test's commit assertion to reflect the dropped Dockerfile. Remaining Phase 2 (follow-ups): submit-time reject/warn on tenant build files; update the starter template (Forgejo civitai-apps/starter) + blocks-cli scaffold + SDK docs to not emit a Dockerfile. See datapacket-talos/claudedocs/app-blocks-a8-phase2-civitai-web-followup-2026-06-03.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): anonymous conversion — REQUEST_SIGN_IN + anon-safe token mint App Blocks "anonymous conversion": a logged-out viewer sees the full block rendered (from the scope-free BLOCK_INIT context); clicking an action that needs auth/money (Generate) prompts sign-in instead of erroring. Token mint (src/pages/api/v1/block-tokens/index.ts): - Replace the hardcoded `isModerator` mint gate with a feature-availability check on the `appBlocks` flag evaluated for the (possibly-null) session. Prod flag is `availability: ['mod']`, so behaviour is UNCHANGED (only mods mint); when the flag goes public, anon/non-mod can mint. Rate-limits + banned/deleted checks unchanged. - Anon (userId == null): instead of 403-ing on `:self` scopes, issue the anon-safe subset = manifest scopes with every consent-gated scope STRIPPED (the COMPLEMENT of CONSENT_EXEMPT, via consentGatedScopes). This withholds every `:self`/owned/money/tip scope (ai:write:budgeted, buzz:read:self, user:read:self, social:tip:self, media:read:owned, models:read:self). For generate-from-model the anon subset is empty → Generate stays server-gated. Fail-closed: any future money/self scope is stripped for anon by default. Host (src/components/AppBlocks/IframeHost.tsx + requestSignInGate.ts): - New inbound REQUEST_SIGN_IN handler (payload {returnUrl?}). Pinned by usePostMessage (origin + event.source) and gated on status==='ready' (post-BLOCK_READY) via the pure resolveRequestSignIn gate; triggers the civitai LoginModal (reason 'image-gen'). returnUrl is open-redirect-guarded (same-origin in-app path only), defaulting to the current page otherwise. Tests (vitest): anon mint strips money/self scopes (not 403) when appBlocks is available; anon mint rejected when appBlocks NOT available; authed-mod mint unchanged; REQUEST_SIGN_IN honored only after BLOCK_READY + returnUrl sanitised. Prod `appBlocks: ['mod']` flag left AS-IS (not flipped). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(app-blocks): collapse failed blocks to null instead of a broken card A block that fails to load (timeout / fatal / no_token / token_error / bad manifest src) showed a visible BlockFallback card. Change every terminal- failure path to render null so the slot collapses and takes no space — a failed block shows nothing. - IframeHost: terminal-failure branches (malformed src, 'timeout', 'fatal', 'no_token') now return null. Decision extracted to the pure, unit-tested hostRenderDecision helper (node-env testable; mirrors the W7/W8 sortInstallsForSlot / failureSnapshot pure-helper pattern). - BlockHost: the token-mint error path (the 'authorization error' card) returns null too — that's the primary token_error fallback. - Preserve the W7 trust chrome on the READY state and the brief loading skeleton during 'loading'. Rendering null on failure shows no content, so the FRAME-1 anti-spoofing property is not weakened (nothing to masquerade as); no reserved min-height gap on failure (the slot's loading reservation is a transient pre-data state, gone once installs resolve). Tests: hostRenderDecision asserts each terminal-failure status collapses and ready/loading render content (60 AppBlocks helper tests green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(app-blocks): gate listForModel/getShowcaseImages by appBlocks flag (not isModerator) + null-safe showcase reactionCount Two anon-conversion bugs — the feature relaxed the appBlocks flag to public + the block-token mint, but two server reads still hard-gated on isModerator, so anon viewers' blocks rendered (flag public) yet never received data: 1. blocks.listForModel returned [] for any non-moderator BEFORE calling BlockRegistry.listForModel — anon never got installs (blocks invisible). Now gates on ctx.features.appBlocks (mirrors the client useFeatureFlags() gate + the mint gate); viewerUserId tolerates anon (ctx.user?.id ?? null). Prod-safe: appBlocks is ['mod'] in prod, so non-mods still get [] pre-GA. 2. blocks.getShowcaseImages threw FORBIDDEN for non-mods AND 500'd on reactionCount: (a) same flag-gate fix (returns [] when flag off, so anon blocks can load showcase once public); (b) ImageMetric.reactionCount is declared non-nullable Int in schema.prisma (no @default) but is NULL in prod, so the typed select threw 'Error converting field reactionCount ... found null' (P2032). Fetch counts via null-tolerant $queryRaw + ?? 0 (the author's original intent) instead of the typed metrics relation. Root-caused via Loki structured logging on the pr-2447 instrumented build. * fix(app-blocks): gate getEffectiveCheckpoint by appBlocks flag too (anon checkpoint resolution) Third instance of the same Phase-2 moderator-gate the anon-conversion feature missed: blocks.getEffectiveCheckpoint threw FORBIDDEN for non-mods, so an anon viewer's block errored on checkpoint resolution (console error on the rendered block) even with the flag public. Now gates on ctx.features.appBlocks (returns {checkpoint:null} → block falls back to the platform per-ecosystem default); getEffectiveCheckpoint already accepts userId: number|null so anon is null-safe. Completes the anon read path alongside listForModel + getShowcaseImages. * fix(app-blocks): un-gate grantScopes (consent) from moderator → protected + flag Fourth Phase-2 mod-gate the anon-conversion missed: blocks.grantScopes was a moderatorProcedure, so a logged-in NON-MOD viewer could never grant the consent-gated scopes their block needs (ai:write:budgeted etc.) — meaning the A6 consent flow could surface 'needs_consent' but the viewer had no way to actually consent, and the block could never spend their buzz. Now protectedProcedure (authenticated) + ctx.features.appBlocks gate. Grant stays bounded to the app's approved manifest ∩ approvedScopes ceiling and writes only the caller's own app_user_scope_grants row. * fix(app-blocks): move pages/api __tests__ out of src/pages for Next 16 build The merge brought main's Next 14 -> 16 upgrade. Next 16's `next build` type-checks every file under src/pages/** as a route, so the 4 App-Blocks handler tests under src/pages/api/**/__tests__/ failed ("does not satisfy ApiRouteConfig") — gotcha #45. Moved them to src/tests/api/** (the existing convention, e.g. src/tests/api/v1/images) and switched the two relative handler imports to ~/pages/... absolute paths. No behavior change; restores a green Next-16 build. * fix(app-blocks): repoint moved test imports to ~/pages absolute paths Follow-up to the test-file move (d05e6ac0c): block-tokens/index.test.ts (25 dynamic import()/vi.mock refs) + developer/block-manifests.test.ts (3) + the two internal/blocks tests still referenced their handlers via relative '../' paths, which broke once moved out of src/pages → TS2307 on typecheck. Repointed all to ~/pages/api/... absolute aliases. * feat(app-blocks): make models:read:self consent-exempt (allow-by-default) Step 1 of the lazy-consent UX: models:read:self is a low-sensitivity read of the viewer's own models (no-op for anon → safe in an anon token), so it no longer requires a per-user grant. The block can render fully for a logged-in viewer with no upfront consent prompt; the consent gate is now reserved for the money/ AI scopes (ai:write:budgeted, buzz:read:self). Step 2 (request those lazily on the Generate click instead of on load) is the block + IframeHost follow-up. * feat(app-blocks): lazy consent — request scopes on the action, not on load The block now renders in full for a logged-in viewer who hasn't granted every consent-gated scope; consent is requested when they click an action that needs it (e.g. Generate), not via an at-load Alert. - IframeHost: trim the wrapped token's `scopes` to what the mint actually signed (manifest scopes minus `missingScopes`) so the block's capability check is accurate; gate buzzBudget on the granted scopes; add a REQUEST_CONSENT handler that opens BlockConsentModal for the server-known missing set, grants via blocks.grantScopes, and re-mints (TOKEN_REFRESH carries the new scopes → the block retries). Gated on status==='ready' via the pure resolveRequestConsent helper. - BlockHost: drop the at-load BlockConsentPrompt; pass missingScopes + onConsentGranted(refresh) to IframeHost. Removes the now-dead BlockConsentPrompt component. - BlockConsentModal: point-of-action consent modal (replaces the Alert). - Fix two stale block-tokens mint tests that predated 01ea90441 making models:read:self consent-exempt (anon + revoked-grant now keep it). 89 AppBlocks/block-tokens tests pass; tsc adds no new errors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): "Hide app block" — viewer-local dismiss of owner-installed blocks A model owner's "show on my models" block renders to every viewer. Add a "Hide app block" item to the host trust-frame's ⋯ menu so a viewer can locally dismiss one without affecting the publisher's install or anyone else. - hiddenBlocks.ts: localStorage-backed (per blockInstanceId), SSR-safe, reactive via an in-page event + cross-tab `storage`. hideBlock() + isBlockHidden() + useHiddenBlocks(). - IframeHost/AppBlockChrome: the new menu item calls hideBlock(instanceId). - BlockSlotClient: filters hidden installs out of the render list, so a hidden block unmounts immediately AND never mounts (no token mint) on reload; an all-hidden slot collapses to nothing. Note: no unhide UI yet — recoverable only by clearing localStorage. + hiddenBlocks unit tests (happy-dom). Typecheck + AppBlocks tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(app-blocks): "Hidden" tab on /apps/installed to restore hidden blocks Pairs with the ⋯-menu "Hide app block": gives viewers a way back. The hidden store now keeps a little metadata per instance (app + model name, hidden-at) so the restore list reads meaningfully with no server lookup; back-compat reader migrates the original string[] shape. - hiddenBlocks.ts: record shape + unhideBlock() + useHiddenBlockList(); hideBlock() now takes a HiddenBlock (instanceId + app/model labels). - IframeHost/AppBlockChrome: pass app + model context into hideBlock. - /apps/installed: new "Hidden" tab listing hidden blocks with a Restore button (reactive — restoring re-shows the block on its model page). + migration + unhide tests. Typecheck + AppBlocks/Apps suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test: add registerInstrumentationMetric/Histogram/GaugeWithLabels to prom mock Merge follow-up: main's eventloop-longtask.ts registers a histogram + counter via registerInstrumentationMetric AT MODULE LOAD, and trpc.ts imports it — so every router test (e.g. blocks.router.*) loads it. The global prom/client mock in setup.ts predated those exports, so the tests failed at import with `No "registerInstrumentationMetric" export defined on the mock`. Add the three metric-factory helpers (additive). * fix(app-blocks): un-gate the /apps/installed own-data procs (moderator→protected) The manage-page queries listMySubscriptions / listMyScopeGrants / listMyAppActivity / listMyScopeInvocations + the own-data management actions uninstallFromModel / setSubscriptionPinnedVersion were moderatorProcedure (the internal-only "remove/relax at GA" gate). But /apps/installed gates per-user on features.appBlocks, so on flag-public surfaces (preview/GA) the page admits non-mods while every tab's query threw FORBIDDEN. Relax to protectedProcedure + the existing enforceAppBlocksFlag middleware (gotcha #66 pattern). Prod-safe: the page's per-user flag still blocks non-mods there; each proc is self-scoped — the reads to ctx.user.id, uninstallFromModel via assertCanManageBlocks (model-owner-or-mod), setSubscriptionPinnedVersion via the service's 'not the subscription owner' guard. Install/upsert/ delete + the mod-review queue + revenue/apps stay mod-gated. + flip the stale listMySubscriptions→FORBIDDEN test to a non-mod success assertion. 99 blocks.router/user-app-surface tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): fix two stale pre-existing test files (scope-grant, showcase) Pre-existing failures on feat (unrelated to the merge), mopped up: - scope-grant.service.test: 01ea90441 made models:read:self consent-exempt, but partitionByConsent / consentGatedScopes still asserted the old exempt set. models:read:self now signs without a grant + is dropped from the gated set. (Same staleness already fixed in block-tokens/index.test.) - showcase.service.test: the reactionCount-is-NULL P2032 fix moved reaction counts to a raw $queryRaw using Prisma.join — which the test never mocked (Prisma.join undefined in the test env). Mock Prisma.join + derive the AllTime ImageMetric rows from the findMany fixture in $queryRaw, so the existing imageRow(...) call sites are unchanged. 432 App-Blocks router/service/component tests now green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(app-blocks): render the model.sidebar_top block below the carousel on mobile On small screens the sidebar grid column stacks full-width, and a mobile-only ModelCarousel renders inside it. The BlockSlot sat ABOVE that carousel, so the app block pushed the image carousel down the page. Move the BlockSlot to just after the mobile carousel: on small screens the block now sits BELOW the carousel; on sm+ the mobile carousel renders nothing, so the block keeps its sidebar-top position (the gallery is in the other grid column). Pure reorder — no behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(app-blocks): fix faulty storage-provision rollback assertion + add merge audit The 'rolls back when a statement throws' test matched the throwing DDL with sql.startsWith('CREATE TABLE …'), but the service emits that DDL as an indented template literal (leading whitespace), so the mock never threw and provision() resolved instead of rejecting. Fix the matcher to trimStart().startsWith(...). The production rollback path (COMMIT in try / catch ROLLBACK+throw / finally release) was already correct — this was a test-only bug now caught by main's full-vitest CI gate (#2489). Also add docs/features/app-blocks-merge-audit-2026-06.md capturing the pre-merge audit (gating/H2 flag-divergence, security, DB/migrations, money paths). * refactor(app-blocks): isolate 72mb body limit to a dedicated upload route The W1 publish-request bundle (base64 ZIP, ~67 MiB encoded) was the only payload exceeding the shared tRPC body limit, and accommodating it had lifted /api/trpc/[trpc] to 72mb for EVERY tRPC call app-wide. Move the upload to a dedicated POST /api/blocks/submit-version route: - 72mb body limit isolated to this one endpoint - ModEndpoint (moderator session) + appBlocks-flag gate + bundle-storage check — auth/behaviour parity with the former blocks.submitVersion tRPC mutation - delegates to the unchanged submitVersion service Revert /api/trpc/[trpc] to 17mb; remove the now-dead blocks.submitVersion tRPC procedure + its unused schema import; rewire /apps/submit to POST the route via a react-query useMutation (same .mutate/.isPending semantics). Verified submitVersion was the only >17mb tRPC path (KV storage.set capped at 64KB). Service tests unchanged (72 pass). * test(app-blocks): handler coverage for POST /api/blocks/submit-version Covers the route's auth/flag/validation shell (the only new logic from the body-limit isolation): ModEndpoint moderator gate (405 non-POST, 401 no-session /non-mod/banned), appBlocks flag (503), bundle-storage precondition (412), schema validation (400 empty/missing), success (200 — service called with the decoded buffer + moderator id), and service-error mapping (400 w/ message). Drives the real ModEndpoint so the gate is genuinely exercised; mocks only auth/env/flag/infra + the (separately-tested) submitVersion service. * test(app-blocks): fix submit-version test to not drive the real withAxiom The first version mocked @civitai/next-axiom, but ModEndpoint's withAxiom closure is captured at endpoint-helpers module-load — in the full-suite run (shared module registry) that load can happen via another file before the per-file mock applies, so the REAL withAxiom ran and hit res.once (passed in isolation, failed in CI). Follow the repo's retool-endpoint.test.ts convention: mock ~/server/utils/endpoint-helpers and provide a ModEndpoint stub that reproduces the real gate verbatim (method→405; session+isModerator+!banned→401). The flag/storage/validation/decode/service branches still run the real handler body. Verified across a 28-file/394-test multi-file run. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: zach <zach@civitai.com>
2026-06-13 10:20:57 -05:00
"@electric-sql/pglite": "^0.4.6",
2024-10-18 16:18:17 -04:00
"@faker-js/faker": "^9.0.3",
"@ladle/react": "^5.1.1",
Migrate to Next.js 15 Bump next 14.2.28 -> 15.5.19 (Pages Router app). Also bumps eslint-config-next and @next/eslint-plugin-next to 15.5.19, and eslint 8.22.0 -> 8.57.1 (Next 15's plugin rules use context.filename, added in ESLint 8.40). Config / API changes required by Next 15: - next.config.mjs: experimental.serverComponentsExternalPackages -> top-level serverExternalPackages; removed the now-stable experimental.instrumentationHook flag. - .eslintrc.js: added settings.next.rootDir so the Next ESLint plugin can locate the pages dir (otherwise the rules crash on load). - bot-detection.middleware.ts: dropped request.ip (NextRequest.ip removed in 15); cf-connecting-ip / x-forwarded-for fallbacks remain. - api/trpc/[trpc].ts: assert the default export as NextApiHandler. Next 15's route-type validator rejected it because withAxiom's NextConfig overload (declared first) structurally matches an arrow function. - Moved api/auth/oauth/__tests__/token.test.ts -> server/oauth/__tests__/token-endpoint.test.ts. Next 15 type-validates every file under pages/api as a route; the colocated test had no default export. Tests still pass (8/8). - Deleted the unused src/app/layout.tsx stub (whole app is Pages Router); removes the spurious "i18n unsupported in App Router" build warning. Build memory + Windows local-build helpers: - package.json build:dev / build:analyze heap 8192 -> 16384 (the app needs >8GB to compile). - scripts/graceful-fs-patch.cjs: optional NODE_OPTIONS --require preload to mitigate Windows EMFILE during local builds (no-op on Linux/CI). Validated: typecheck clean; lint at parity with main; build compiles, type-checks, passes route validation and page-data collection. The full production build's "Collecting build traces" step is FD-bound on Windows (EMFILE) and should be run on CI/Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:08:40 -06:00
"@next/eslint-plugin-next": "^15.5.19",
"@playwright/test": "^1.57.0",
"@prisma/generator-helper": "^5.22.0",
"@types/archiver": "^6.0.2",
2023-05-23 15:14:42 -06:00
"@types/cloudflare": "^2.7.9",
feat(app-blocks): line-level code diff in moderator review UI (#2831) The /apps/review ReviewModal previously showed only a FILE-level diff (added/changed/removed paths + counts) and a manifest field diff — a mod could see WHICH files changed but had to click out to Forgejo to read the actual code. This closes that "see exactly what changed" gap with an in-modal per-file unified line diff. Server: - computeBundleLineDiff: pure, IO-free per-file unified diff (via the `diff` lib's structuredPatch) between the pending bundle and the previous approved version. First version = whole-file adds. Hard bounds (the key correctness concern — never load unbounded content into memory/the response): TEXT FILES ONLY (binary by extension OR NUL-byte sniff is skipped), per-file 256 KiB byte cap, per-file 2000-line diff cap, and a 300-file total cap. Every elided file is explicitly marked (binary / too-large / diff-too-large / file-cap) so the UI shows the Forgejo fallback instead of silently dropping a change. - blocks.getPublishRequestDiff: moderator-gated tRPC query mirroring the auth/shape of getPublishRequestScreenshots (moderatorProcedure + isModerator belt + enforceAppBlocksFlag). Reuses the existing MinIO/Forgejo bundle-fetch helpers; fetches the previous approved bundle's bytes (excluding self) to diff against. UI: - ReviewModal gains a lazy "Show code diff" toggle under the file-diff list (query only fires when toggled). Each changed/added text file expands to a styled unified diff (+/- lines); elided files render a "view in Forgejo" fallback. Consistent with the existing review styling. Deps: promotes the already-transitively-pinned `diff@4.0.2` to a direct dependency + adds matching `@types/diff@4.0.2` (zero new resolution). Tests: 10 new unit tests for computeBundleLineDiff covering text-vs-binary detection (extension + NUL sniff), first-version all-add, a changed file's expected unified hunks, and every size/line/file-cap elision path. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 10:28:47 -05:00
"@types/diff": "4.0.2",
2024-02-19 10:07:13 -05:00
"@types/file-saver": "^2.0.7",
2024-04-08 15:49:31 -06:00
"@types/he": "^1.2.3",
2024-03-13 14:36:08 -04:00
"@types/html-to-text": "^9.0.4",
"@types/js-yaml": "^4.0.9",
2023-07-17 21:15:16 -06:00
"@types/jsonwebtoken": "^9.0.2",
2023-03-28 09:00:14 +00:00
"@types/lodash-es": "^4.17.7",
2023-09-23 14:20:33 -06:00
"@types/mailchimp__mailchimp_marketing": "^3.0.12",
2023-01-09 16:53:29 -07:00
"@types/marked": "^4.0.7",
"@types/node": "24.13.3",
2023-04-20 11:07:36 -06:00
"@types/node-os-utils": "^1.3.1",
2023-01-02 15:59:36 -07:00
"@types/nodemailer": "^6.4.7",
2024-02-20 16:52:19 -07:00
"@types/offscreencanvas": "^2019.7.3",
2024-02-01 22:23:36 -07:00
"@types/pg": "^8.11.0",
"@types/pg-format": "^1.0.5",
2023-09-18 18:23:03 -04:00
"@types/randomstring": "^1.1.8",
2022-10-11 16:56:51 -04:00
"@types/react": "18.0.14",
"@types/react-dom": "18.0.5",
"@types/request-ip": "^0.0.37",
2022-11-08 19:09:38 -04:00
"@types/sanitize-html": "^2.6.2",
"@types/semver": "^7.7.1",
2022-11-14 21:39:23 -07:00
"@types/sharp": "^0.31.0",
"@types/three": "^0.180.0",
"@types/uuid": "^9.0.0",
"@types/vimeo__player": "^2.18.3",
"@types/xml2js": "^0.4.14",
2022-10-11 16:56:51 -04:00
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
test(component): Vitest browser-mode component-testing scaffold (runs in Tekton, removes GH Actions pr-check) (#2547) * test(component): add Vitest browser-mode component-testing scaffold + SeedInput Adds a second Vitest project (`component`, browser mode / real Chromium via Playwright) alongside the unchanged 857-test `unit` suite. Includes a renderWithProviders scaffold (Mantine + QueryClient + next/router mock), a process.env shim for browser mode, a report-only GH Actions `component-tests` job, and the first test on the high-churn, e2e-impossible SeedInput generation leaf (6 cases, mutation-proven). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(component): address audit findings (typecheck test/, seed-test teeth, optimizeDeps) - tsconfig: add `test` to include so the load-bearing browser-process-shim + component-setup are actually typechecked (were only checked transitively/not at all). (audit H1) - SeedInput test: stub Math.random for an exact-value assertion bounded by MAX_RANDOM_SEED (was a loose MAX_SEED range that survived a constant-seed mutation). (audit M1) - vitest component project: optimizeDeps.include next/router to stop the "Vite unexpectedly reloaded a test" flake warning. (audit L4) - drop the stale "857 tests" count from the config comment. (audit M2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: remove GitHub Actions pr-check.yml — consolidate PR checks onto Tekton typecheck + unit-tests already run in the Tekton PR-preview pipeline (author- gated on MEMBER/OWNER/COLLABORATOR), and component-tests now run there too (talos-infra: report-only npm-component-tests task). Removing this workflow stops paying for GitHub Actions runner time. Tradeoff: external-contributor PRs (not author-authorized) no longer get automated checks — accepted per the "pure Tekton" decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:55:14 -05:00
"@vitest/browser": "^4.0.18",
"@vitest/browser-playwright": "4.0.18",
"@vitest/coverage-v8": "^4.0.18",
"autoprefixer": "^10.4.19",
"cross-env": "^7.0.3",
2024-05-16 16:46:10 -06:00
"cssnano": "^7.0.1",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"esbuild": "^0.25.5",
Migrate to Next.js 15 Bump next 14.2.28 -> 15.5.19 (Pages Router app). Also bumps eslint-config-next and @next/eslint-plugin-next to 15.5.19, and eslint 8.22.0 -> 8.57.1 (Next 15's plugin rules use context.filename, added in ESLint 8.40). Config / API changes required by Next 15: - next.config.mjs: experimental.serverComponentsExternalPackages -> top-level serverExternalPackages; removed the now-stable experimental.instrumentationHook flag. - .eslintrc.js: added settings.next.rootDir so the Next ESLint plugin can locate the pages dir (otherwise the rules crash on load). - bot-detection.middleware.ts: dropped request.ip (NextRequest.ip removed in 15); cf-connecting-ip / x-forwarded-for fallbacks remain. - api/trpc/[trpc].ts: assert the default export as NextApiHandler. Next 15's route-type validator rejected it because withAxiom's NextConfig overload (declared first) structurally matches an arrow function. - Moved api/auth/oauth/__tests__/token.test.ts -> server/oauth/__tests__/token-endpoint.test.ts. Next 15 type-validates every file under pages/api as a route; the colocated test had no default export. Tests still pass (8/8). - Deleted the unused src/app/layout.tsx stub (whole app is Pages Router); removes the spurious "i18n unsupported in App Router" build warning. Build memory + Windows local-build helpers: - package.json build:dev / build:analyze heap 8192 -> 16384 (the app needs >8GB to compile). - scripts/graceful-fs-patch.cjs: optional NODE_OPTIONS --require preload to mitigate Windows EMFILE during local builds (no-op on Linux/CI). Validated: typecheck clean; lint at parity with main; build compiles, type-checks, passes route validation and page-data collection. The full production build's "Collecting build traces" step is FD-bound on Windows (EMFILE) and should be run on CI/Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 12:08:40 -06:00
"eslint": "8.57.1",
"eslint-config-next": "^15.5.19",
2022-10-11 16:56:51 -04:00
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
feat(lint): no-io-in-transaction rule + clear existing violations (#2382) * feat(lint): no-io-in-transaction rule + clear existing violations Adds a custom ESLint rule (eslint-local-rules.js) that flags awaited external/non-DB I/O inside a Prisma interactive `$transaction(async (tx) => …)` callback. Such calls (HTTP fetch, image scanner, Buzz API, Axiom logging, Redis cache busts, search-index queueing) add their latency to the txn's wall-clock timeout budget and, when slow, blow it ("Transaction already closed"). This is the recurring class behind #2375 / #2377 / #2379 — the rule turns "sweep it again" into "caught in review/IDE". Detection is a curated denylist of known I/O call names (low false-positive); calls on the tx client itself (`tx.*`, `tx.$queryRaw`/`$executeRaw`) are always allowed. Validated with a RuleTester suite (10 cases). Wiring is conditional in .eslintrc.js: the rule activates automatically once `eslint-plugin-local-rules` is installed (`pnpm add -D eslint-plugin-local- rules`) and is skipped until then, so `next lint` keeps working and CI's `pnpm install --frozen-lockfile` is unaffected (no package.json/lockfile change in this PR — pnpm wasn't available to regenerate the lockfile). Brings the codebase to a clean baseline for the rule: Fixed (moved external work after commit / made fire-and-forget): - collection/article(x2)/model(x2)/bountyEntry: userXCountCache.refresh() (Redis) moved to after the txn commits, using the returned row's id. - referral/redeemableCode: error-branch logToAxiom() de-awaited (Axiom HTTP), matching the #2379 pattern (.catch retained / added). Ratchet-disabled with TODO(tx-io) (intentional / needs careful change): - bounty.createBounty + bountyEntry.awardBountyEntry: Buzz charge/settlement inside the txn — moving needs charge→tx→refund-on-failure compensation (a PG rollback can't undo an external Buzz charge); left for a domain owner. - report.createReport CSAM branch: search-index delete inside the txn — moving needs hoisting the CSAM guard post-commit on a sensitive path. tsc --noEmit error-neutral vs baseline across all touched files (pre-existing Prisma-client-drift errors unchanged; CI regenerates the client). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(lint): audit follow-ups — install plugin, warn-level, create-only refresh, tests Addresses the audit of #2382: - H1: install `eslint-plugin-local-rules` (devDep + lockfile via pnpm --lockfile-only) and wire the rule unconditionally. Previously the rule was only activated when the plugin happened to resolve, but the 6 `// eslint-disable-next-line local-rules/no-io-in-transaction` directives error with "Definition for rule not found" in ESLint 8 when the rule is unconfigured — so `next lint` broke in the plugin-absent state. With the plugin now a real dependency the rule is configured and the directives are valid. - Rule severity set to `warn` (not `error`): surfaces in the editor / next lint as a guardrail without failing lint or the build; escalate later. - M1: bountyEntry.upsertBountyEntry count-cache refresh is now gated to the create path (`!id`). The pre-move code only refreshed in the create branch; the first move ran it on updates too (extra primary-DB COUNT + Redis on every description edit). Restored create-only semantics. - Test coverage: add src/server/services/__tests__/no-io-in-transaction.test.ts (RuleTester via vitest, 20 cases incl. FP/FN regression guards). Runs with `pnpm test:unit:run`; 20/20 pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(pr-check): gate the no-io-in-transaction rule via its RuleTester suite Adds a `test:lint-rules` script (vitest run of the rule's test) and a "Test lint rules" step to the pr-check workflow, right after typecheck (reuses the job's already-installed deps). Scoped to the rule's own test rather than the full `test:unit:run` suite: the full suite currently has pre-existing failures (e.g. a timezone-dependent redeemableCode date assertion) and isn't CI-green, so wiring it wholesale would block all PRs. This step deterministically gates the custom rule's correctness; broadening to the full suite is a separate cleanup once those failures are fixed. Note: the rule itself is `warn`-level, so `next lint` surfaces violations without failing the build — this CI step gates the RULE (regressions in eslint-local-rules.js), not new violations. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: retrigger preview (prior build hit transient pnpm-install network timeouts) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:55:26 -05:00
"eslint-plugin-local-rules": "^3.0.2",
2024-05-16 16:46:10 -06:00
"eslint-plugin-tailwindcss": "^3.15.1",
2024-07-25 09:40:42 -04:00
"husky": "^9.1.1",
"pg-format": "^1.0.4",
"playwright": "^1.57.0",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"postcss": "^8.5.3",
"postcss-assign-layer": "^0.4.0",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
2024-05-16 16:46:10 -06:00
"prettier": "^2.8.8",
"prisma": "^6.3.0",
"prisma-generator-typescript-interfaces": "^1.6.1",
"prisma-kysely": "^2.2.0",
"tailwindcss": "^3.4.3",
2022-10-14 11:05:04 -06:00
"ts-node": "^10.9.1",
"tsx": "^4.19.2",
"turbo": "^2.9.17",
Mantine v7 Migration (#1717) * Start migration, start updating files & details * Attempt at migrating ContainerGrid styles * Continue migration * Migrate tag/[tagname] * Checkpoint: Before the adsProvider migration * General progress - migrate complex AdUnit useStyles * Migrates VotableTags component to mantine v7 * Migrate card styles, profile styles and more * More migrations * Nits * Migrates components to mantine v7 * Migrate all articles components * Updates User components to mantine v7 * Cleanup to files up to ChatList.tsx * Checkpoint: Container Grid * Migrate up to CosmeticShopItemUpsertForm * Mantine migration: Training and subscription components * Fixes alpha color mix in css modules * Checkpoint up to QuickSearchDropdown * Checkpoint up to RouterTransition * Checkpoint up to ImageResources * Fixes after merging with main * Checkpoint - main components * Some pages * Fix minor nit * Completes migration of pages components * 2nd pass of fixes' * More fixes after typecheck * Last type fixes * Somehow, manage initial render * Minor nits, initial render, improvements * Update colors in theme * Minor changes and improvements * Fixes app header and footer styles * Replaces sx and text color everywhere * Fixes media queries * Fixes ContainerGrid * Fixes ActionIcon styles everywhere * Fixes alpha color mix in css modules * Fixes dark/light theme not working with tailwind * Fixes after merging with main * Fixes bad css module imports * Fixes issues preventing build * Main fixes to account and model feed pages * Fixes after merging with main * Fixes animation module file typo * Fixes to feeds * Fixes after merging with main * Fixes icons in menu items * Fixes homeblocks and other things * Fixes to AuctionFiltersDropdown * Fixes home page and most feed cards * Fixes most styles in the generation form * Fixes gen panel * Fixes shop styles * Updates package-lock file * Fixes after merging with main * Creator program nits * Fixes search page styles * Several fixes to small pages * Fixes css type issues * Fixes reviews page and css modules types issues * Fixes lock file and tailwind vars * Fixes package-lock * More fixes to package-lock * Buzz dashboard nits * Minor model form nits * Fixes model details page and richTextEditor styles * Start working on the training page * Brings back old color scheme * loader types + auction * Fixes most forms * Fixes type issues * Adjusts text link color * Fixes collections layout and styles * Final touches to the image detail page * Fixes after merging with main * Fix autocomplete search * Small tweaks and fixes * More style fixes * Fixes notifications and navigation progress * Fixes after merging with main * Adds creatable multiselect component * Fixes grouped select input * Adjusts aria-label for close buttons in modal * More small fixes * Fixes nits from review * Bunch of fixes all around * Fixes after merging with main * Another wave of fixes * Fixes after merging with main * Bunch of updates based off review feedback * General fixes * Finishes migrating all user facing pages and components * Fixes type issues * Fixes admin pages * Last round of feedback incoming --------- Co-authored-by: Luis Rojas <lrojas94@gmail.com> Co-authored-by: Brett Woodward <flipperbw@gmail.com>
2025-06-18 15:20:59 -04:00
"typed-scss-modules": "^8.1.1",
"typescript": "^5.9.2",
"vitest": "^4.0.18",
test(component): Vitest browser-mode component-testing scaffold (runs in Tekton, removes GH Actions pr-check) (#2547) * test(component): add Vitest browser-mode component-testing scaffold + SeedInput Adds a second Vitest project (`component`, browser mode / real Chromium via Playwright) alongside the unchanged 857-test `unit` suite. Includes a renderWithProviders scaffold (Mantine + QueryClient + next/router mock), a process.env shim for browser mode, a report-only GH Actions `component-tests` job, and the first test on the high-churn, e2e-impossible SeedInput generation leaf (6 cases, mutation-proven). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(component): address audit findings (typecheck test/, seed-test teeth, optimizeDeps) - tsconfig: add `test` to include so the load-bearing browser-process-shim + component-setup are actually typechecked (were only checked transitively/not at all). (audit H1) - SeedInput test: stub Math.random for an exact-value assertion bounded by MAX_RANDOM_SEED (was a loose MAX_SEED range that survived a constant-seed mutation). (audit M1) - vitest component project: optimizeDeps.include next/router to stop the "Vite unexpectedly reloaded a test" flake warning. (audit L4) - drop the stale "857 tests" count from the config comment. (audit M2) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: remove GitHub Actions pr-check.yml — consolidate PR checks onto Tekton typecheck + unit-tests already run in the Tekton PR-preview pipeline (author- gated on MEMBER/OWNER/COLLABORATOR), and component-tests now run there too (talos-infra: report-only npm-component-tests task). Removing this workflow stops paying for GitHub Actions runner time. Tradeoff: external-contributor PRs (not author-authorized) no longer get automated checks — accepted per the "pure Tekton" decision. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 14:55:14 -05:00
"vitest-browser-react": "^2.2.0",
"ws": "^8.19.0"
2022-10-11 16:56:51 -04:00
},
"ct3aMetadata": {
"initVersion": "6.2.1"
},
chore(lint): add CI job, drop type-aware override and prettier plugin (#3362) * chore(lint): add CI job, drop type-aware override and prettier plugin `pnpm lint` took ~2h40m and 61% of its findings duplicated `prettier:check`. Nothing enforced lint, typecheck, or formatting in CI, which is how a config-load crash survived ~2 months unnoticed. - Remove the `*.ts`/`*.tsx` `parserOptions.project` override. Its own `rules` block was empty; the only type-aware rule anywhere was root-level `@typescript-eslint/restrict-template-expressions` (warn), which `tsc --noEmit` largely subsumes. Both are gone. Measured on src/utils (82 files): 4m34s -> 8.4s. Full `pnpm lint`: ~2h40m -> 1m07s. - Drop eslint-plugin-prettier and the `prettier/prettier` rule. `pnpm prettier:check` already globs `**/*.{ts,tsx}` with no .prettierignore, a strict superset of lint's `src/`-only scope, with matching options. `eslint-config-prettier` stays in `extends`. - Add .github/workflows/lint.yml: ESLint + Prettier on PR-changed files only, full typecheck in a parallel job. - Delete unreferenced devDeps (airbnb, airbnb-typescript, mantine configs, eslint-import-resolver-typescript, lint-staged) and the lint-staged config block, which invoked `tsc-files` — a package that was never installed. - Pin eslint-config-next to major 15 via pnpm.overrides; v16 is flat-config-only and silently kills lint when extended from eslintrc. - Delete the top-level npm-style `overrides.openai.zod` block. pnpm reads pnpm.overrides, so it never applied; openai@4 declares zod ^3.23.8 as an optional peer while the repo is on zod 4, so honoring it would assert a compatibility that does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(lint): make CI lint steps report-only, skip typecheck on forks Review fixes for the CI job. - ESLint and Prettier steps get `continue-on-error`. For a formatter, "changed file" means the whole file: 789 of 4,116 src files fail `prettier --check` today. Across the 98 src files touched by the last 30 commits, 39 fail Prettier and 10 carry a pre-existing ESLint error - 44 (45%) would red the job for reasons unrelated to the PR, forcing 3-line bugfixes to ship as 200-line reformats. Annotations still surface. Both flip to blocking once the backlog clears with the Prettier 2->3 upgrade. Typecheck stays blocking; it is clean today and is the real guard. - Skip the whole typecheck job on fork PRs. civitai/civitai is public and a fork gets no secrets, so ssh-agent hard-fails on an empty key. Gating only the ssh/submodule steps trades that for a wall of missing-module errors, so the job is skipped rather than half-run. - Drop `--depth=1` from the base fetch; `fetch-depth: 0` already has the ref and the graft can break the merge-base walk on a conflicting PR. - Bump typecheck timeout 15 -> 25 minutes. - Fix the header comment, which conflated changed-file with changed-line. - Remove the commented-out lint-staged block and tsc-files note from .husky/pre-commit, left over from the dep removed in the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(lint): annotate findings, block on newly added files Re-review fixes. - Add .github/problem-matchers/eslint-unix.json and register it, so ESLint findings become file annotations on the PR diff. The previous header claimed "annotations still surface" - they did not. There was no matcher anywhere in .github/, and neither setup-node nor action-setup registers one, so findings only ever reached a step log inside a green check. Prettier annotates via ::warning / ::error commands. - Split the lint steps by diff filter. Newly ADDED files (--diff-filter=A) are BLOCKING; modified/renamed (CMR) stay report-only. This stops the backlog growing while the full flip waits on the Prettier 2->3 upgrade. Added files are checked for errors only, without --max-warnings: the repo carries 3,470 warnings and 1,762 uses of `any`, so failing on warnings would hold new files to a stricter bar than anything already merged. - Qualify the header claim about typecheck being the real gate; it is skipped on fork PRs, so a fork gets no gating from this workflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 16:47:33 -06:00
"//eslint-config-next": "Stay on major 15. v16 is flat-config-only (peer eslint >=9); extending it from .eslintrc.js makes @eslint/eslintrc reject it and then crash while formatting the error, so lint silently never runs. Pinned below so a `pnpm up` can't walk it forward.",
"pnpm": {
"overrides": {
chore(lint): add CI job, drop type-aware override and prettier plugin (#3362) * chore(lint): add CI job, drop type-aware override and prettier plugin `pnpm lint` took ~2h40m and 61% of its findings duplicated `prettier:check`. Nothing enforced lint, typecheck, or formatting in CI, which is how a config-load crash survived ~2 months unnoticed. - Remove the `*.ts`/`*.tsx` `parserOptions.project` override. Its own `rules` block was empty; the only type-aware rule anywhere was root-level `@typescript-eslint/restrict-template-expressions` (warn), which `tsc --noEmit` largely subsumes. Both are gone. Measured on src/utils (82 files): 4m34s -> 8.4s. Full `pnpm lint`: ~2h40m -> 1m07s. - Drop eslint-plugin-prettier and the `prettier/prettier` rule. `pnpm prettier:check` already globs `**/*.{ts,tsx}` with no .prettierignore, a strict superset of lint's `src/`-only scope, with matching options. `eslint-config-prettier` stays in `extends`. - Add .github/workflows/lint.yml: ESLint + Prettier on PR-changed files only, full typecheck in a parallel job. - Delete unreferenced devDeps (airbnb, airbnb-typescript, mantine configs, eslint-import-resolver-typescript, lint-staged) and the lint-staged config block, which invoked `tsc-files` — a package that was never installed. - Pin eslint-config-next to major 15 via pnpm.overrides; v16 is flat-config-only and silently kills lint when extended from eslintrc. - Delete the top-level npm-style `overrides.openai.zod` block. pnpm reads pnpm.overrides, so it never applied; openai@4 declares zod ^3.23.8 as an optional peer while the repo is on zod 4, so honoring it would assert a compatibility that does not exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(lint): make CI lint steps report-only, skip typecheck on forks Review fixes for the CI job. - ESLint and Prettier steps get `continue-on-error`. For a formatter, "changed file" means the whole file: 789 of 4,116 src files fail `prettier --check` today. Across the 98 src files touched by the last 30 commits, 39 fail Prettier and 10 carry a pre-existing ESLint error - 44 (45%) would red the job for reasons unrelated to the PR, forcing 3-line bugfixes to ship as 200-line reformats. Annotations still surface. Both flip to blocking once the backlog clears with the Prettier 2->3 upgrade. Typecheck stays blocking; it is clean today and is the real guard. - Skip the whole typecheck job on fork PRs. civitai/civitai is public and a fork gets no secrets, so ssh-agent hard-fails on an empty key. Gating only the ssh/submodule steps trades that for a wall of missing-module errors, so the job is skipped rather than half-run. - Drop `--depth=1` from the base fetch; `fetch-depth: 0` already has the ref and the graft can break the merge-base walk on a conflicting PR. - Bump typecheck timeout 15 -> 25 minutes. - Fix the header comment, which conflated changed-file with changed-line. - Remove the commented-out lint-staged block and tsc-files note from .husky/pre-commit, left over from the dep removed in the previous commit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(lint): annotate findings, block on newly added files Re-review fixes. - Add .github/problem-matchers/eslint-unix.json and register it, so ESLint findings become file annotations on the PR diff. The previous header claimed "annotations still surface" - they did not. There was no matcher anywhere in .github/, and neither setup-node nor action-setup registers one, so findings only ever reached a step log inside a green check. Prettier annotates via ::warning / ::error commands. - Split the lint steps by diff filter. Newly ADDED files (--diff-filter=A) are BLOCKING; modified/renamed (CMR) stay report-only. This stops the backlog growing while the full flip waits on the Prettier 2->3 upgrade. Added files are checked for errors only, without --max-warnings: the repo carries 3,470 warnings and 1,762 uses of `any`, so failing on warnings would hold new files to a stricter bar than anything already merged. - Qualify the header claim about typecheck being the real gate; it is skipped on fork PRs, so a fork gets no gating from this workflow. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 16:47:33 -06:00
"eslint-config-next": "15",
"vite": "6.4.3",
"protobufjs@7": "^7.5.6",
"fast-xml-parser": "^5.9.3",
"@aws-sdk/core>fast-xml-parser": "5.2.5",
"axios": "^1.16.0",
"undici@6": "^6.27.0",
"tar-fs@2": "^2.1.5",
"tar-fs@3": "^3.1.3",
"ws@7": "^7.5.11"
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"@prisma/client",
"@prisma/engines",
"bigint-buffer",
"bufferutil",
"core-js",
"esbuild",
"exifreader",
"msgpackr-extract",
"prisma",
"protobufjs",
"sharp",
"unrs-resolver",
"utf-8-validate"
2026-04-28 12:44:44 -06:00
],
"patchedDependencies": {
"@mantine/hooks": "patches/@mantine__hooks.patch"
}
2022-10-11 16:56:51 -04:00
}
2026-06-30 15:06:24 -06:00
}