batchUpdateCustomFields used $executeRaw to merge a manager-supplied
record straight into Resource.dynamicFields with no key whitelist —
so a manager could pollute the JSONB namespace with arbitrary keys
(e.g. ones admin tools later interpret). Separately, several user-facing
JSONB fields (allocation/demand metadata, dynamicFields) were typed as
unbounded z.record(z.string(), z.unknown()), letting clients ship
multi-MB payloads that flow into DB writes, audit logs, and SSE frames.
- Add BoundedJsonRecord helper (shared) — 64 keys / depth 4 /
8 KB strings / 32 KB serialized total. Conservative defaults; call
sites needing more should use a strict object schema.
- Apply BoundedJsonRecord to the highest-traffic untrusted JSONB inputs:
allocation metadata (Create/CreateDemandRequirement/CreateAssignment),
resource & project dynamicFields, and the createDemand router input.
- batchUpdateCustomFields:
* Tighten input schema (key length, value bounds, max 100 keys).
* Fetch each target resource and verify all input keys are in the
union of (specific blueprint defs) ∪ (active global RESOURCE
blueprint defs) for that resource. Empty whitelist → reject all
keys (stricter than create/update, but appropriate for a bulk
escape-hatch endpoint).
* Run the existing per-key value validator afterwards.
* 404 if any requested id does not exist (was silently skipped).
- New helper getAllowedDynamicFieldKeys() in blueprint-validation.
- 7 new BoundedJsonRecord tests, 2 new batchUpdateCustomFields tests
covering the whitelist-rejection and not-found paths.
Covers EAPPS 3.2.7 (input bounds) / OWASP A03 (injection / mass assignment).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The read-only proxy previously wrapped model delegates to block writes,
but left client-level raw/escape hatches ($transaction, $executeRaw,
$executeRawUnsafe, $queryRawUnsafe, $runCommandRaw) intact. A read-tool
could smuggle DML via raw SQL, or open an interactive $transaction whose
tx-scoped client (unproxied by construction) accepts writes.
- read-only-prisma: block $transaction, $executeRaw, $executeRawUnsafe,
$queryRawUnsafe, $runCommandRaw at the client level. Template-tagged
$queryRaw stays allowed (read-only by API contract).
- assistant-tools: add create_estimate to MUTATION_TOOLS — it uses
$transaction internally and was previously bypassing the proxy only
because $transaction wasn't blocked.
- shared: document isReadOnly flag on ToolContext so any scoped tRPC
caller a tool spawns keeps the proxied client.
- helpers: note the runtime wrap at assistant-tools.ts:739 is
authoritative; forwarding ctx.db verbatim is correct.
- tests: cover model writes, raw escapes, and the allowed $queryRaw
path (7 cases, all pass).
- loosen one estimate-detail test that compared the exact db instance
(fails once that instance is a proxy; the assertion's intent is the
estimate id).
Covers EGAI 4.1.1.2 / IAAI 3.6.22.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rate-limiter now accepts string | string[] so callers can key on
multiple buckets simultaneously. If any bucket is exhausted the
request is denied, which lets login/TOTP/reset-password throttle on
BOTH user identifier and source IP without either becoming a bypass.
Fail-closed: empty/whitespace-only keys now deny by default instead
of silently allowing unbounded attempts (was CWE-307 gap).
Degraded-fallback divisor reduced from /10 to /2 — the old aggressive
clamp forced-logged-out legitimate users during brief Redis outages;
/2 still meaningfully slows distributed brute-force.
Callers updated:
- auth.ts (login): both email: and ip: buckets
- auth router requestPasswordReset: email + IP
- auth router resetPassword: IP before lookup, email-reset after
- invite router getInvite/acceptInvite: IP
- user-self-service verifyTotp: userId + IP
TRPCContext now carries clientIp; web tRPC route extracts it from
X-Forwarded-For / X-Real-IP.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
#36 CRITICAL: add .max(128) to all password Zod schemas to prevent
Argon2-based DoS from unbounded password strings.
#46 HIGH: configure pino redact paths so passwords/tokens/cookies/TOTP
secrets are never serialized in logs.
#58 MEDIUM: upgrade dompurify to ^3.4.0 and add pnpm overrides for
brace-expansion (>=5.0.5) and esbuild (>=0.25.0) to patch known CVEs.
Vite moderate (path traversal, dev-only) remains — requires vitest 3.x
major upgrade, deferred.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
CI unit-test runs vitest run --coverage in each workspace package, but only
apps/web declared the coverage-v8 dep. In pnpm workspaces deps aren't
hoisted across packages, so engine/staffing/api/application/shared need it
directly.
The build job also needs REDIS_URL because collecting page data for
/api/perf imports a module that throws if REDIS_URL is missing under
NODE_ENV=production. A placeholder value satisfies the check (no actual
Redis connection is made at build time).
Committed assistant-tools.ts already references toolDefinition?.resultSchema
for EGAI 4.3.1.2 result validation, but the ToolDef interface in shared.ts
was missing the field declaration, breaking typecheck.
- Remove host port mappings from postgres/redis services in ci.yml;
QNAP runner already occupies 5432. Use service DNS names
(postgres/redis) instead of localhost for DB/Redis URLs.
- Track packages/api/src/lib/read-only-prisma.ts which was imported
by assistant-tools.ts but never committed, breaking check:imports.
Replace z.unknown() with z.union([z.string(), z.number(), z.boolean(), z.null()])
to constrain what values can be written into the dynamicFields jsonb column via
the $executeRaw path. Prevents arbitrary nested structures from being serialized.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- timeline-holiday-load-support: deduplicate getResolvedCalendarHolidays
by location key so resources sharing the same country/state/city resolve
holidays once instead of per-resource
- rate-card-lookup: add lookupRatesBatch that loads rate card lines once
and scores locally per demand line, replacing per-line DB round-trips
in estimate-demand-lines autoFillDemandLineRates
- config-readmodels: include _count in utilization-category list query
instead of calling getById per category for project counts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace z.array(z.unknown()) with RolePresetsSchema for blueprint
role presets mutation input, ensuring structural validation before
Prisma JSON cast. Also adds SECURITY.md for vulnerability disclosure.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add indexes on Resource(blueprintId, roleId), DemandRequirement(roleId),
Assignment(roleId) — commonly filtered FK columns that were missing indexes
- Replace N+1 batch delete pattern (2N queries) with findAllocationEntries()
that does 2 total queries via findMany({ id: { in: ids } })
- Add take/skip pagination with default limit of 500 to listDemands and
listAssignments to prevent unbounded result sets
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Move CI_AUTH_SECRET from plaintext to ${{ secrets.CI_AUTH_SECRET }}
- Wrap password reset (update + session kill + token mark) in $transaction
to prevent stale sessions on partial failure (CWE-613)
- Rate limiter Redis fallback now uses stricter degraded limits
(maxRequests/10) and logs at error level instead of warn
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers estimate list, getById, version snapshot aggregation, rethrowEstimateRouterError,
submit/approve/createRevision workflow procedures. Vacation read covers isSameUtcDay,
list, getById, getForResource, team overlap, and team overlap detail.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers comment CRUD/resolve/delete, project status transitions and cascade
deletes, dispo import batch read/cancel/commit/resolve, and holiday calendar
catalog read with identifier fallback lookup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 3c continued: covers admin settings CRUD with secret handling,
webhook lifecycle with SSRF validation, and calculation rules with
controller/manager authorization boundaries.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 3c: covers list/getById/create/update for all three routers
including authorization guards, conflict detection, NOT_FOUND errors,
and audit logging verification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests fell behind source changes: lastTotpAt replay-attack prevention,
activeSession invalidation on password reset, select clauses in
permission updates, UNAUTHORIZED (anti-enumeration) for disabled TOTP,
and password minimum raised from 8 to 12 characters.
Also fix root eslint.config.mjs to ignore packages/ (linted via turbo)
and add --no-warn-ignored to lint-staged to suppress warnings for
ignored files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast Zod schemas with .refine()/.superRefine() to z.ZodType<InferredType> at the
procedure level. This short-circuits TypeScript's deep type recursion through
tRPC's middleware chain, eliminating 4 of 5 @ts-expect-error TS2589 suppressions
in web components (VacationModal, ProjectModal, UsersClient, CountriesClient).
Applied same pattern to allocation, timeline, staffing, dashboard, project, and
resource query/mutation procedures to reduce client-side type depth.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace ~440 lines of hand-written structural DB client types across 7 lib files
with `Pick<PrismaClient, ...>` from @capakraken/db. This eliminates all `as any`
casts at Prisma boundaries (cron routes, allocation effects, vacation procedures)
and surfaces two pre-existing bugs:
- weekly-digest.ts: `db.allocation.count()` called non-existent model (fixed → demandRequirement)
- estimate-reminders.ts: `submittedAt` field doesn't exist on EstimateVersion (fixed → updatedAt)
Also adds root eslint.config.mjs so lint-staged can lint package files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Password validation: min(8) → min(12) across auth.ts, user-procedure-support.ts,
and invite.ts (aligns with NIST SP 800-63B modern recommendations)
- Error boundary: stop rendering raw error.message which could leak internal
details; always show the generic fallback text
- Add `pnpm audit` script (--audit-level=high) for dependency vulnerability scanning
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds lastTotpAt timestamp to User model. After a successful TOTP validation,
the timestamp is recorded. Any reuse of the same code within the 30-second
window is rejected as a replay attack.
verifyTotp now returns a single generic UNAUTHORIZED error regardless of
whether the user ID is invalid or TOTP is not enabled, preventing enumeration
of user IDs and MFA status.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- blueprint rolePresets: cap array at 100 items to prevent storage abuse
- notification CreateManagedNotification: add .max() on title (500),
body (2000), type (100), entityType/entityId (200), link (1000),
taskAction (200)
- settings: add .max() on all string config fields; add regex allowlist
(/^[a-zA-Z0-9._-]+$/) on model name fields (geminiModel,
azureDalleDeployment, azureOpenAiDeployment) to prevent path manipulation
- sanitizeHtml: fix SSR bypass — server-side branch now strips HTML tags
instead of returning the raw string unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- setUserPassword and resetPassword now call activeSession.deleteMany after
updating the passwordHash, so any pre-change sessions are immediately revoked
(CWE-613 session fixation after credential change)
- setUserPermissions and resetUserPermissions now use explicit Prisma select to
exclude passwordHash and totpSecret from the returned user object
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move core entitlement business logic (syncEntitlement, balance reading,
year summary, set/bulk-set) into packages/application/src/use-cases/entitlement/
using the deps-injection pattern. Audit logging stays in the router support
file; authorization check for getBalance/getBalanceDetail stays in the router
layer. The router support file becomes a thin wiring adapter.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add deletedAt DateTime? to User, Client, Role, Resource, and Blueprint
models for GDPR-compliant deactivation audit trail. Soft-delete mutations
now stamp deletedAt: new Date() on deactivation and clear it on
reactivation. Migration and test assertions updated accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P2002/P2025/P2003 now map to CONFLICT/NOT_FOUND/BAD_REQUEST with generic
messages. Raw Prisma error details no longer reach the client.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Tests expected include: { resourceRoles } but the Prisma select audit
changed the query to select: { ...RESOURCE_LIST_SELECT, resourceRoles }.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces full model includes with field-scoped selects on the resource
list (listStaff) query. Avoids fetching large JSONB columns
(availability, valueScoreBreakdown) and unused scalar fields (aiSummary,
portfolioUrl, fte, resourceType, postalCode, etc.) when only
identity/rate fields are needed.
Adds RESOURCE_LIST_SELECT constant to packages/api/src/db/selects.ts
covering all fields actually consumed by ResourcesClient, FillOpenDemandModal,
EstimateWizard, EstimateWorkspaceDraftEditor, and ScenarioPlanner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Procedures were re-fetching the acting user from DB using the session
email, which breaks if email changes between session creation and request.
ctx.dbUser is populated by protectedProcedure and is always current.
Also removed the now-unused findVacationActor helper function.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves approve, reject, cancel, and request vacation business logic
out of the tRPC procedure layer into packages/application, matching
the pattern used by allocation use-cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Throw at startup in production if REDIS_URL/DATABASE_URL/NEXTAUTH_SECRET missing
- Warn in development when REDIS_URL falls back to localhost
- QueryClient: add gcTime, disable refetchOnWindowFocus, skip retry on 4xx
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents mutations from committing without an audit trail if the
auditLog.create call fails after the main write already succeeded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves read, assignment-procedures, assignment-mutations, and demand
procedures into allocation/ so the domain boundary is discoverable
without grep.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- dailyCostCents, hoursPerDay, percentage now validated at API boundary
- vacation router no longer uses ctx.db as any
- scenarioData reads through typed Zod schema
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>