Skip to content

Add Resend password reset flow - #234

Open
lumenstech wants to merge 104 commits into
trycompai:mainfrom
lumenstech:codex/forgot-password-resend
Open

lumenstech wants to merge 104 commits into
trycompai:mainfrom
lumenstech:codex/forgot-password-resend

Conversation

@lumenstech

@lumenstech lumenstech commented Sep 24, 2026 •

Copy link
Copy Markdown

Summary

  • add Better Auth password reset email support backed by Resend
  • add forgot-password and reset-password UI flows with neutral request messaging
  • document required Resend environment variables without committing production values

Verification

  • bunx biome check on touched files
  • bun run check-types
  • bun test packages/auth/test/mailbox-grant.spec.ts packages/auth/test/signed-in.spec.ts
  • bun test apps/app/test/api-proxy-response.spec.ts apps/app/test/onboarding-gate.spec.ts apps/app/test/landing-analytics.spec.ts
  • bun --env-file=.env run build

Notes

  • Do not deploy until production has RESEND_API_KEY and PASSWORD_RESET_FROM configured. PASSWORD_RESET_REPLY_TO is optional but recommended.
  • Full repo-wide lint still has pre-existing unrelated failures in @crm/db.

Summary by cubic

Adds self-service password reset backed by Resend, with forgot-password and reset-password pages and a change-password card in settings. Email/password login is now enabled; previously no password-based sign-in existed, and users who forgot their password had no recovery path.

Do not deploy until production sets RESEND_API_KEY and PASSWORD_RESET_FROM; PASSWORD_RESET_REPLY_TO is optional.

Migration

  • Run scripts/deploy-production-db-migrations.sh to apply the new Prisma migrations before deploying the API.

Notes

  • This PR also carries the larger release branch it was based on: signal ingest and intelligence inbox, opportunity scoring and review, the scheduled Guyana opportunity collector, the Data-Gear procurement module, and Outlook history backfill.

Written for commit c60bef3. Summary will update on new commits.

Review in cubic

carhartlewis and others added 30 commits August 7, 2026 11:38
lumenstech and others added 26 commits September 23, 2026 08:17
* Add procurement pricing models

* Add procurement pricing migration

* Add procurement pricing helper

* Export procurement pricing helper

* Expose procurement pricing module
* Add procurement server actions

* Add procurement CRM page

* Scope procurement records by business unit

* Scope procurement migration by business unit

* Add Data-Gear procurement business unit helper

* Bind procurement actions to Data-Gear business unit

* Filter procurement page to Data-Gear business unit

* Add Procurement to primary CRM navigation

* Recreate procurement views when adding business unit scope

* Format procurement pricing helper

* Match Biome procurement pricing formatting

* Format procurement server actions

* Format procurement CRM page

* Match Biome procurement page formatting

* Format Procurement navigation item

* Format Data-Gear procurement helper
* Fix Procurement runtime rendering

* Format Procurement runtime boundary

* Fix Procurement content indentation

* Match Biome Procurement runtime formatting
* Enable email password authentication

* Add email password sign in form

* Show email password login on sign in page
release: Outlook history backfill
* Add password reset email flow

* Add password change settings card

* Add forgot password request form

* Keep login password independent of env secrets

* Remove incomplete reset flow

* Add password management to settings
@vercel

vercel Bot commented Sep 24, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the Comp AI - PoC Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions
github-actions Bot changed the base branch from release to main September 24, 2026 20:57
@github-actions

Copy link
Copy Markdown
Contributor

Retargeted this onto main.

release is the default branch so that a plain clone runs the last tagged release, but nothing merges into it — it is fast-forwarded onto the tag by the Release workflow and that is all. Changes go to main, and reach release when a release is cut.

Nothing is wrong with your branch. If the diff now shows commits that are already on main, rebase and force-push:

git fetch origin main
git rebase origin/main
git push --force-with-lease

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

60 issues found across 58 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/auth/src/auth.ts">

<violation number="1" location="packages/auth/src/auth.ts:82">
P0: `enabled: true` exposes password signup without email verification. Because the existing allow-list checks only the domain, anyone can register as another workspace address and receive a session without proving ownership; disable password signup or add a verified-email flow before enabling it.</violation>
</file>

<file name="apps/app/app/(landing)/forgot-password/forgot-password-form.tsx">

<violation number="1" location="apps/app/app/(landing)/forgot-password/forgot-password-form.tsx:19">
P1: The new password-reset form is unreachable for signed-out users: the app proxy redirects `/forgot-password` (and `/reset-password`) to `/sign-in` before rendering it. Add both password-reset paths to the unauthenticated allowlist.</violation>

<violation number="2" location="apps/app/app/(landing)/forgot-password/forgot-password-form.tsx:33">
P2: This discards `requestPasswordReset` errors, so Resend/API outages are shown as successful requests and the form becomes non-retryable. Keep unknown-address responses neutral, but show a generic delivery error and leave the form available when the reset request actually fails.</violation>
</file>

<file name="packages/db/prisma/schema.prisma">

<violation number="1" location="packages/db/prisma/schema.prisma:1755">
P1: `SourceRecord` is tenant-scoped by `businessUnitId`, but this unique key is global. `IngestService.signal` uses the same three columns for lookup and `ON CONFLICT`, so an identical source ID in another project overwrites the first unit’s payload and ownership; include the business unit in the source and mapping conflict keys.</violation>

<violation number="2" location="packages/db/prisma/schema.prisma:1810">
P2: This compound unique does not deduplicate company-only or person-only members because PostgreSQL treats NULLs as distinct. Use separate partial unique indexes for company and person targets, and reject members whose two target IDs are both null.</violation>

<violation number="3" location="packages/db/prisma/schema.prisma:1898">
P2: The migration creates a one-default-per-business-unit unique index, but this model only declares a non-unique index. Prisma schema synchronization can treat that database guard as unmanaged and remove it; declare the partial unique constraint here.</violation>
</file>

<file name="apps/api/src/ingest/opportunity-ops.service.ts">

<violation number="1" location="apps/api/src/ingest/opportunity-ops.service.ts:154">
P1: `decide` accepts a caller-controlled reviewer ID, so any authenticated user can record an approval or rejection as another CRM user. Derive the reviewer ID from the authenticated session, or compare the submitted ID with the session actor before inserting the audit event.</violation>

<violation number="2" location="apps/api/src/ingest/opportunity-ops.service.ts:233">
P1: `recordPromotion` trusts an approval fetched earlier, so a concurrent rejection can land before this insert and the promotion still succeeds. Revalidate the exact approved decision in the same transaction as promotion, or use a conditional database write that fails when the decision is no longer approved.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx:216">
P1: The sheet preserves `score` and `ownerId` across different signals, so opening another signal can submit the previous signal's values. Remount or reset the review state whenever `sourceRecordId` changes.</violation>

<violation number="2" location="apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx:220">
P1: Successful resolve and qualify mutations refresh the list but never update the selected `SignalRow`. The open sheet therefore keeps stale score and mapping gates, so users must close and reopen it before they can promote the newly resolved or scored signal.</violation>
</file>

<file name="scripts/deploy-production-db-migrations.sh">

<violation number="1" location="scripts/deploy-production-db-migrations.sh:1">
P2: This file is tracked as mode `100644`, so `./scripts/deploy-production-db-migrations.sh` returns permission denied before Bash reads the shebang. Mark it executable as `100755`.</violation>

<violation number="2" location="scripts/deploy-production-db-migrations.sh:4">
P2: This production deploy script defaults `REPO_DIR` and `ENV_FILE` to absolute personal-machine paths (`/Users/danny/Documents/Codex/...`, including a directory literally named `2026-09-02-you-are-operating-on-my-mac`). Any operator or CI run without those exact arguments targets a nonexistent path, and the commit leaks the author's private filesystem layout into a public repo. Require both arguments (error out when unset) or derive defaults from the script's own location and the repo root.</violation>

<violation number="3" location="scripts/deploy-production-db-migrations.sh:15">
P1: `git rev-parse HEAD` only prints the selected checkout; it does not verify `release` or a clean worktree. A feature checkout can apply migrations before matching code reaches production, so validate the release ref before `db:deploy`.</violation>

<violation number="4" location="scripts/deploy-production-db-migrations.sh:28">
P1: An exported `DATABASE_URL` can take precedence over the supplied `--env-file`, so this can migrate the caller’s database instead of the file’s database. Clear the inherited variable before all three Bun invocations.</violation>
</file>

<file name="apps/api/src/ingest/guyana-opportunity.service.ts">

<violation number="1" location="apps/api/src/ingest/guyana-opportunity.service.ts:74">
P1: This deduplication path reuses a time-dependent evaluation. After an unchanged opportunity’s deadline passes, it still returns the pre-expiration score, recommendation, and `hardBlocked` value; re-evaluate when current blockers differ.</violation>
</file>

<file name="packages/db/prisma/migrations/20260907130000_opportunity_ops_review/migration.sql">

<violation number="1" location="packages/db/prisma/migrations/20260907130000_opportunity_ops_review/migration.sql:1">
P1: This migration creates `opportunity_review_event` outside `schema.prisma`, so the repository's post-migration drift check will reject every database containing this table. Add a matching Prisma model (or otherwise account for this table in the schema/drift strategy) before shipping.</violation>

<violation number="2" location="packages/db/prisma/migrations/20260907130000_opportunity_ops_review/migration.sql:94">
P2: The queue can attribute an old business-unit review to the source record's current business unit because the lateral event lookup ignores `r."businessUnitId"`. Match the event and source business units here, and enforce the same-unit relationship for writes, so re-ingesting a source under another project cannot expose or mislabel its prior review.</violation>
</file>

<file name="apps/api/src/microsoft/outlook-sync.service.ts">

<violation number="1" location="apps/api/src/microsoft/outlook-sync.service.ts:193">
P1: The backfill can loop forever on a burst of more than 120 messages within the one-second overlap window. Each tick re-queries the same first 120 messages, so the cursor never reaches the remaining messages or `COMPLETE`; persist a stable page/composite cursor instead of using only an overlapped timestamp.</violation>
</file>

<file name="apps/api/src/mailbox/sync-state.service.ts">

<violation number="1" location="apps/api/src/mailbox/sync-state.service.ts:201">
P1: `advanceBackfill` accepts stale workers without fencing them to the backfill run that produced the update. Carry a run/version or lease through start, advance, and failure updates so an older batch cannot overwrite or complete a newer import.</violation>
</file>

<file name="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql">

<violation number="1" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:2">
P2: This migration uses non-idempotent DDL (plain `CREATE TABLE`, `ADD COLUMN`) for `business_unit`, `canonical_company`, `source_record`, `agentTask.businessUnitId`, etc., yet the pre-existing migration `20260907130000_opportunity_ops_review` (already in this migrations folder, not in this batch) creates foreign keys referencing `business_unit`, `source_record`, and `canonical_opportunity`. Any database that has already applied `opportunity_ops_review` must already contain those tables (created out-of-band), so running `prisma migrate deploy` — exactly what the new `scripts/deploy-production-db-migrations.sh` does — will fail there with duplicate table/column errors. Verify the production schema state before deploying; if the tables exist, reconcile with `prisma migrate resolve --applied 20260902184536_sequencenow_crm_foundation` or make the DDL idempotent (CREATE TABLE IF NOT EXISTS / ADD COLUMN IF NOT EXISTS).</violation>

<violation number="2" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:84">
P2: These target IDs are not referentially constrained, so inserts can create orphaned campaign members or approvals and deleting a person/company leaves stale rows. Add nullable foreign keys to the canonical target tables and to `campaign_member.memberId`, with the intended `ON DELETE` behavior.</violation>

<violation number="3" location="packages/db/prisma/migrations/20260902184536_sequencenow_crm_foundation/migration.sql:291">
P1: These foreign keys check existence but not tenant consistency. An opportunity or source record in one business unit can reference a company or pipeline in another; add composite tenant-aware foreign keys or database checks for every cross-entity link.</violation>
</file>

<file name="ops/macos/install-guyana-opportunity-collector.sh">

<violation number="1" location="ops/macos/install-guyana-opportunity-collector.sh:1">
P2: The installer is not executable in Git, so invoking it through its shebang fails with `Permission denied`. Mark this file executable in the repository.</violation>

<violation number="2" location="ops/macos/install-guyana-opportunity-collector.sh:21">
P2: The installed job ignores `GUYANA_COLLECTOR_ENV_FILE`, so custom environment files work only during manual runner invocations and scheduled runs use the default path. Add the selected path to the plist's `EnvironmentVariables`.</violation>

<violation number="3" location="ops/macos/install-guyana-opportunity-collector.sh:24">
P2: The generated plist is invalid when a dynamic path contains XML-reserved characters. Escape XML values or construct the plist with a plist-aware tool before bootstrapping it.</violation>
</file>

<file name="apps/api/src/ingest/ingest.router.ts">

<violation number="1" location="apps/api/src/ingest/ingest.router.ts:67">
P2: These new procedures are missing from the tracked generated `AppRouter`, which is the type contract exported to the app. Regenerate `src/generated` so the Guyana and opportunity-ops endpoints are available to typed clients.</violation>
</file>

<file name="apps/api/src/ingest/ingest.service.ts">

<violation number="1" location="apps/api/src/ingest/ingest.service.ts:140">
P2: This cast accepts arbitrary JSON text; a payload with `metadata.fit_score: "high"` makes the entire inbox query fail with a database cast error. Guard the cast with numeric validation in all three score expressions.</violation>

<violation number="2" location="apps/api/src/ingest/ingest.service.ts:157">
P2: `SignalInboxInput.source` is never applied, so requesting one source returns signals from every source. Add a predicate on `sr."sourceSystem"`.</violation>

<violation number="3" location="apps/api/src/ingest/ingest.service.ts:211">
P2: This limit runs before `scoreCandidate` and sorting, so with more than 20 matches the endpoint can omit the exact-name or same-business-unit candidate. Rank before limiting, or sort all matches then slice to 20.</violation>

<violation number="4" location="apps/api/src/ingest/ingest.service.ts:376">
P2: This ingest module performs identity matching and opportunity scoring in the Nest API, but AGENTS.md requires intelligence to live in `apps/agent`. Route these decisions through an `AgentTask` instead.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/procurement/actions.ts">

<violation number="1" location="apps/app/app/(app)/[slug]/procurement/actions.ts:16">
P2: `money` accepts zero and fractions, but this action uses it for strictly positive quantities and integer fields. A zero quantity stores an unusable procurement record, while fractional `leadTimeDays`, `gpuCount`, or `ramGb` causes Prisma to reject the mutation; add field-specific positive/integer validation before creating records.</violation>

<violation number="2" location="apps/app/app/(app)/[slug]/procurement/actions.ts:166">
P2: `requiredBy` accepts any nonempty form string, so malformed direct submissions produce `Invalid Date` and fail the entire customer-request mutation. Parse the date and reject it when `date.getTime()` is `NaN` before calling Prisma.</violation>
</file>

<file name="apps/api/src/ingest/ingest.contracts.ts">

<violation number="1" location="apps/api/src/ingest/ingest.contracts.ts:30">
P1: `sourceUrl` accepts non-HTTP schemes such as `javascript:` and the inbox renders it directly as an anchor. Restrict this field to `http:` and `https:` before persisting it.</violation>
</file>

<file name="apps/api/src/microsoft/microsoft-sync.service.ts">

<violation number="1" location="apps/api/src/microsoft/microsoft-sync.service.ts:53">
P2: `startBackfill` resets an already-running import to the requested `from` date. Because the UI can invoke it again after the request returns, an active import can be rewound and repeatedly re-read, causing duplicate work and throttling; reject or resume an existing `RUNNING` job.</violation>

<violation number="2" location="apps/api/src/microsoft/microsoft-sync.service.ts:56">
P2: `startBackfill` awaits the first batch but discards its outcome. A Graph failure marks the backfill `FAILED`, so future syncs skip it while the route still reports success; propagate the failure or keep the job retryable.</violation>
</file>

<file name="apps/api/src/ingest/signal-qualification.service.ts">

<violation number="1" location="apps/api/src/ingest/signal-qualification.service.ts:128">
P2: This path accepts monetary values larger than the `DECIMAL(14,2)` columns can store. Apply the same finite/max-cents validation before creating or updating the canonical opportunity, rather than passing the unbounded value to SQL and `deals.create`.</violation>
</file>

<file name="apps/app/lib/procurement.ts">

<violation number="1" location="apps/app/lib/procurement.ts:22">
P2: The policy check and create are not atomic. Concurrent first procurement requests can both pass the check, then the partial unique index makes one request fail with `P2002` instead of returning successfully; serialize initialization or use a conflict-safe atomic initialization and re-read.</violation>
</file>

<file name="packages/db/prisma/migrations/20260924111500_procurement_pricing/migration.sql">

<violation number="1" location="packages/db/prisma/migrations/20260924111500_procurement_pricing/migration.sql:57">
P2: `quoteQuantity` accepts zero, so a submitted zero-quantity quote is stored but produces NULL pricing in the matrix. Enforce a positive quantity at the database boundary.</violation>

<violation number="2" location="packages/db/prisma/migrations/20260924111500_procurement_pricing/migration.sql:183">
P2: Changing `procurementPricingPolicy.markupRate` does not affect any matrix price; new quotes keep the quote-column default of 30%. Apply the selected policy markup when creating or calculating quotes.</violation>

<violation number="3" location="packages/db/prisma/migrations/20260924111500_procurement_pricing/migration.sql:256">
P2: `canFulfill` is true for unavailable, RFQ, or stale quotes whenever stock meets demand. Require a `LIVE` pricing status before reporting that a supplier can fulfill the request.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/settings/connections/microsoft-connection.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/settings/connections/microsoft-connection.tsx:323">
P2: This disables the trigger only while the request is pending, but another click during a long import restarts the import from 2000. Reject repeat starts or expose backfill status and disable this action while it is running.</violation>
</file>

<file name="packages/db/src/procurement-pricing.ts">

<violation number="1" location="packages/db/src/procurement-pricing.ts:21">
P2: `roundMoney` can round valid half-cent amounts down because the absolute `Number.EPSILON` adjustment is too small at larger values. For example, schema-valid inputs `10 + 0.15 / 2` return a landed cost of `10.07` instead of `10.08`; use magnitude-aware or decimal-based rounding.</violation>
</file>

<file name="apps/api/scripts/guyana-opportunity-collector.ts">

<violation number="1" location="apps/api/scripts/guyana-opportunity-collector.ts:79">
P2: Following redirects without validating the final host bypasses the collector's approved-source boundary and can send content from an unapproved host to Langflow. Reject redirects or validate `response.url` against the source allowlist after fetching.</violation>

<violation number="2" location="apps/api/scripts/guyana-opportunity-collector.ts:125">
P2: The collector sends the CRM token in `authorization: Bearer`, but the API-key auth configuration reads `x-api-key`, so submissions fail with 401. Send the token as `x-api-key`.</violation>

<violation number="3" location="apps/api/scripts/guyana-opportunity-collector.ts:213">
P2: A run with one failed source and one successful source exits 0, so the scheduled collector reports partial collection as successful. Set the exit status when `summary.some((item) => item.error)` is true.</violation>
</file>

<file name="deploy/mac-mini/service-runner.ts">

<violation number="1" location="deploy/mac-mini/service-runner.ts:19">
P3: The `in` check accepts inherited keys such as `constructor` and `__proto__`. Use `Object.hasOwn(services, service)` so invalid CLI input exits with the documented usage message.</violation>

<violation number="2" location="deploy/mac-mini/service-runner.ts:51">
P2: `child.killed` records that `kill()` was called, not that the child exited. Forward later signals too, so a hung service can still be stopped.</violation>
</file>

<file name="apps/app/app/(app)/procurement/page.tsx">

<violation number="1" location="apps/app/app/(app)/procurement/page.tsx:4">
P2: This redirect page never executes for settled, signed-in users. apps/app/proxy.ts intercepts `/procurement` before the page renders: its `appPath()` treats the path as an unknown top-level route with no rest segment and rewrites it via `workspaceUrl(slug, "/")` to `/{slug}/` (the dashboard) — `/procurement` is not in `SECTIONS` and `first !== slug`. So a signed-in user clicking the "Procurement" rail item (`href: "/procurement"` in app-icon-rail.tsx) is redirected to the dashboard, and this redirect to `/lumenstechnology-com/procurement` is unreachable. Add `/procurement` to `SECTIONS` in apps/app/proxy.ts (which makes the shortcut page itself unnecessary), or resolve the slug here instead.</violation>

<violation number="2" location="apps/app/app/(app)/procurement/page.tsx:4">
P2: The redirect hardcodes the tenant slug instead of resolving the current user's workspace. Whenever the page does run (proxy only forwards `/procurement` when the workspace gate is unsettled or `workspace.slug` is missing), `[slug]/layout.tsx` calls `workspace.slug !== slug → notFound()`, so any user whose workspace slug is not `lumenstechnology-com` gets a 404 at the redirect target. Resolve the signed-in user's actual workspace slug (e.g., via `requireSession()`/`workspace.get`) before redirecting, or drop the shortcut if the procurement route is single-tenant.</violation>
</file>

<file name="packages/db/scripts/outlook-scan-dry-run.ts">

<violation number="1" location="packages/db/scripts/outlook-scan-dry-run.ts:49">
P2: The script defaults to a personal mailbox scan id (`danny@lumenstechnology.com`) and embeds personal domains in `GENERIC_OR_RELAY_DOMAINS`. Anyone else running it without `--scan-id` silently analyzes one specific person's mailbox dump, and the personal email/domains become public repo contents. Require `--scan-id` instead of defaulting to this dump.</violation>

<violation number="2" location="packages/db/scripts/outlook-scan-dry-run.ts:161">
P2: This query targets `outlook_scan_archive`, but that table is not defined in `packages/db/prisma/schema.prisma` nor created by any migration in `packages/db/prisma/migrations/`. The script therefore fails with "relation outlook_scan_archive does not exist" against any migration-managed database and silently depends on unversioned schema drift in the environment it was written against. Version the table (Prisma model + migration) or read this data through a defined model so the dry run works outside the single origin environment.</violation>

<violation number="3" location="packages/db/scripts/outlook-scan-dry-run.ts:177">
P2: The dry run trusts unvalidated JSON as `Finding[]`, so one malformed finding can abort the entire report. Validate each finding's shape before mapping, or reject the archive with a clear validation error.</violation>
</file>

<file name="packages/auth/src/password-reset-email.ts">

<violation number="1" location="packages/auth/src/password-reset-email.ts:19">
P2: The sender reads variables that Turbo strips from the API task environment. Add all three password-reset variables to the relevant Turbo pass-through lists; otherwise `bun run dev` and API task-based deployments always report password reset email as unconfigured.</violation>
</file>

<file name="apps/api/src/ingest/ingest.module.ts">

<violation number="1" location="apps/api/src/ingest/ingest.module.ts:11">
P1: IngestRouter applies @UseMiddlewares(AuthMiddleware), but AuthMiddleware is provided and exported by TrpcModule — which IngestModule does not import. Every sibling module with an AuthMiddleware-protected router (companies/companies.module.ts, deals/deals.module.ts, contacts) imports TrpcModule, and the repo's nestjs-trpc notes require router middlewares to be resolvable providers in the router's module context. Missing the import can fail DI resolution when the ingest router is built, and `bun build` (compile-only) won't surface it. Add TrpcModule to imports.</violation>
</file>

<file name=".env.example">

<violation number="1" location=".env.example:60">
P2: This block calls PASSWORD_RESET_REPLY_TO "optional" without noting the required-together rule, but packages/auth/src/env.ts (passwordReset()) throws at module load when the env is partially set — REPLY_TO alone, or only one of RESEND_API_KEY / PASSWORD_RESET_FROM, makes the API fail to boot (env.ts is evaluated eagerly via the @crm/auth imports in app.module.ts). The Google/Microsoft blocks warn "Set both or neither"; document the same constraint here so a deployer who adds only the recommended REPLY_TO doesn't get a startup crash.</violation>
</file>

<file name="docs/data-gear-gpu-buyer-target-queue-2026-09-23.md">

<violation number="1" location="docs/data-gear-gpu-buyer-target-queue-2026-09-23.md:159">
P2: This file commits personal contact data — named individuals, their roles, and first-name email addresses — plus the recipient list of a completed outreach campaign into the public MIT repo (github.com/trycompai/crm). Keep operational queues and outreach records out of the committed tree. Move them to a gitignored or private location and keep only citation-based sourcing guidance in docs/, which is what existing docs like list-building-roadmap.md already do.</violation>
</file>

<file name="apps/app/app/(landing)/sign-in/email-password-sign-in.tsx">

<violation number="1" location="apps/app/app/(landing)/sign-in/email-password-sign-in.tsx:69">
P2: These email, password, and name fields have no persistent accessible labels, so placeholder text is the only field identification for assistive-technology users. Add visible `FieldLabel`s associated with each input via `id`/`htmlFor`.</violation>
</file>

<file name="apps/app/app/(app)/[slug]/procurement/page.tsx">

<violation number="1" location="apps/app/app/(app)/[slug]/procurement/page.tsx:225">
P2: These supplier, product, availability, and known-product selectors have no accessible name; their placeholder options are not labels. Add an associated `<Label htmlFor>` or an appropriate `aria-label` to each selector.</violation>
</file>

<file name="ops/macos/run-guyana-opportunity-collector.sh">

<violation number="1" location="ops/macos/run-guyana-opportunity-collector.sh:18">
P2: launchd strips the user's shell PATH for gui-domain jobs (default: /usr/bin:/bin:/usr/sbin:/sbin), and the plist defines no EnvironmentVariables, so bare `bun` will be "command not found" unless the collector env file happens to export PATH. The same PR's deploy/mac-mini/service-runner.ts avoids this by spawning `process.execPath` (the absolute bun binary). Add a PATH entry to the plist, or call bun by absolute path (e.g. the path printed by `which bun`).</violation>
</file>

<file name="apps/api/src/ingest/guyana-opportunity.sources.ts">

<violation number="1" location="apps/api/src/ingest/guyana-opportunity.sources.ts:5">
P3: `collectorUrls` is dead registry metadata: the collector always uses `url`, so this field cannot add or change collection targets. Remove the duplicate field or wire the collector to consume it before relying on these lists as configuration.</violation>
</file>

Heads up: you’ve reached your flex budget. Increase your flex budget or wait for usage to reset.

Re-trigger cubic

Comment thread packages/auth/src/auth.ts

emailAndPassword: {
enabled: false,
enabled: true,

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0: enabled: true exposes password signup without email verification. Because the existing allow-list checks only the domain, anyone can register as another workspace address and receive a session without proving ownership; disable password signup or add a verified-email flow before enabling it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/auth/src/auth.ts, line 82:

<comment>`enabled: true` exposes password signup without email verification. Because the existing allow-list checks only the domain, anyone can register as another workspace address and receive a session without proving ownership; disable password signup or add a verified-email flow before enabling it.</comment>

<file context>
@@ -78,7 +79,16 @@ export const auth = betterAuth({
 
 	emailAndPassword: {
-		enabled: false,
+		enabled: true,
+		revokeSessionsOnPasswordReset: true,
+		sendResetPassword: async ({ user, url, token }) => {
</file context>
Suggested change
enabled: true,
enabled: true,
disableSignUp: true,
Fix with cubic

@@ -0,0 +1,86 @@
"use client";

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The new password-reset form is unreachable for signed-out users: the app proxy redirects /forgot-password (and /reset-password) to /sign-in before rendering it. Add both password-reset paths to the unauthenticated allowlist.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(landing)/forgot-password/forgot-password-form.tsx, line 19:

<comment>The new password-reset form is unreachable for signed-out users: the app proxy redirects `/forgot-password` (and `/reset-password`) to `/sign-in` before rendering it. Add both password-reset paths to the unauthenticated allowlist.</comment>

<file context>
@@ -0,0 +1,86 @@
+const NEUTRAL_MESSAGE =
+	"If that address exists, a password reset link has been sent.";
+
+export function ForgotPasswordForm() {
+	const emailId = useId();
+	const [pending, setPending] = useState(false);
</file context>
Fix with cubic

opportunityId String?
opportunity CanonicalOpportunity? @relation(fields: [opportunityId], references: [id], onDelete: SetNull)

@@unique([sourceSystem, sourceType, sourceId])

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: SourceRecord is tenant-scoped by businessUnitId, but this unique key is global. IngestService.signal uses the same three columns for lookup and ON CONFLICT, so an identical source ID in another project overwrites the first unit’s payload and ownership; include the business unit in the source and mapping conflict keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/prisma/schema.prisma, line 1755:

<comment>`SourceRecord` is tenant-scoped by `businessUnitId`, but this unique key is global. `IngestService.signal` uses the same three columns for lookup and `ON CONFLICT`, so an identical source ID in another project overwrites the first unit’s payload and ownership; include the business unit in the source and mapping conflict keys.</comment>

<file context>
@@ -1583,3 +1608,431 @@ model Apikey {
+  opportunityId  String?
+  opportunity    CanonicalOpportunity? @relation(fields: [opportunityId], references: [id], onDelete: SetNull)
+
+  @@unique([sourceSystem, sourceType, sourceId])
+  @@index([businessUnitId, sourceType])
+  @@map("source_record")
</file context>
Fix with cubic

) VALUES (
${reviewEventId}, NULL, ${source.id}, ${source.businessUnitId}, 'decision',
${input.decision}, ${recommendation}, ${latest.score}, NULL, ${input.rationale},
${input.reviewerUserId}, ${decidedAt}, ${immutableHash}, CURRENT_TIMESTAMP

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: decide accepts a caller-controlled reviewer ID, so any authenticated user can record an approval or rejection as another CRM user. Derive the reviewer ID from the authenticated session, or compare the submitted ID with the session actor before inserting the audit event.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/ingest/opportunity-ops.service.ts, line 154:

<comment>`decide` accepts a caller-controlled reviewer ID, so any authenticated user can record an approval or rejection as another CRM user. Derive the reviewer ID from the authenticated session, or compare the submitted ID with the session actor before inserting the audit event.</comment>

<file context>
@@ -0,0 +1,281 @@
+			) VALUES (
+				${reviewEventId}, NULL, ${source.id}, ${source.businessUnitId}, 'decision',
+				${input.decision}, ${recommendation}, ${latest.score}, NULL, ${input.rationale},
+				${input.reviewerUserId}, ${decidedAt}, ${immutableHash}, CURRENT_TIMESTAMP
+			)
+		`;
</file context>
Fix with cubic

</table>
</div>

<SignalReviewSheet

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The sheet preserves score and ownerId across different signals, so opening another signal can submit the previous signal's values. Remount or reset the review state whenever sourceRecordId changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/[slug]/intelligence/intelligence-inbox.tsx, line 216:

<comment>The sheet preserves `score` and `ownerId` across different signals, so opening another signal can submit the previous signal's values. Remount or reset the review state whenever `sourceRecordId` changes.</comment>

<file context>
@@ -0,0 +1,650 @@
+				</table>
+			</div>
+
+			<SignalReviewSheet
+				row={selected}
+				onClose={() => setSelected(null)}
</file context>
Suggested change
<SignalReviewSheet
<SignalReviewSheet key={selected?.sourceRecordId ?? "closed"}
Fix with cubic

name: string;
};

const DEFAULT_SCAN_ID = "outlook:danny@lumenstechnology.com:0-4799";

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The script defaults to a personal mailbox scan id (danny@lumenstechnology.com) and embeds personal domains in GENERIC_OR_RELAY_DOMAINS. Anyone else running it without --scan-id silently analyzes one specific person's mailbox dump, and the personal email/domains become public repo contents. Require --scan-id instead of defaulting to this dump.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/db/scripts/outlook-scan-dry-run.ts, line 49:

<comment>The script defaults to a personal mailbox scan id (`danny@lumenstechnology.com`) and embeds personal domains in `GENERIC_OR_RELAY_DOMAINS`. Anyone else running it without `--scan-id` silently analyzes one specific person's mailbox dump, and the personal email/domains become public repo contents. Require `--scan-id` instead of defaulting to this dump.</comment>

<file context>
@@ -0,0 +1,394 @@
+	name: string;
+};
+
+const DEFAULT_SCAN_ID = "outlook:danny@lumenstechnology.com:0-4799";
+
+function arg(name: string): string | undefined {
</file context>
Fix with cubic

import { redirect } from "next/navigation";

export default function ProcurementShortcutPage() {
redirect("/lumenstechnology-com/procurement");

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The redirect hardcodes the tenant slug instead of resolving the current user's workspace. Whenever the page does run (proxy only forwards /procurement when the workspace gate is unsettled or workspace.slug is missing), [slug]/layout.tsx calls workspace.slug !== slug → notFound(), so any user whose workspace slug is not lumenstechnology-com gets a 404 at the redirect target. Resolve the signed-in user's actual workspace slug (e.g., via requireSession()/workspace.get) before redirecting, or drop the shortcut if the procurement route is single-tenant.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/app/app/(app)/procurement/page.tsx, line 4:

<comment>The redirect hardcodes the tenant slug instead of resolving the current user's workspace. Whenever the page does run (proxy only forwards `/procurement` when the workspace gate is unsettled or `workspace.slug` is missing), `[slug]/layout.tsx` calls `workspace.slug !== slug → notFound()`, so any user whose workspace slug is not `lumenstechnology-com` gets a 404 at the redirect target. Resolve the signed-in user's actual workspace slug (e.g., via `requireSession()`/`workspace.get`) before redirecting, or drop the shortcut if the procurement route is single-tenant.</comment>

<file context>
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function ProcurementShortcutPage() {
+	redirect("/lumenstechnology-com/procurement");
+}
</file context>
Fix with cubic

return null;
}

private scoreCandidate(

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This ingest module performs identity matching and opportunity scoring in the Nest API, but AGENTS.md requires intelligence to live in apps/agent. Route these decisions through an AgentTask instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/ingest/ingest.service.ts, line 376:

<comment>This ingest module performs identity matching and opportunity scoring in the Nest API, but AGENTS.md requires intelligence to live in `apps/agent`. Route these decisions through an `AgentTask` instead.</comment>

<file context>
@@ -0,0 +1,415 @@
+		return null;
+	}
+
+	private scoreCandidate(
+		row: CandidateRow,
+		entity: string | null,
</file context>
Fix with cubic

"government-eprocure": {
name: "Guyana Government eProcure",
url: "https://eprocure.gov.gy/",
collectorUrls: ["https://eprocure.gov.gy/"],

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: collectorUrls is dead registry metadata: the collector always uses url, so this field cannot add or change collection targets. Remove the duplicate field or wire the collector to consume it before relying on these lists as configuration.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/api/src/ingest/guyana-opportunity.sources.ts, line 5:

<comment>`collectorUrls` is dead registry metadata: the collector always uses `url`, so this field cannot add or change collection targets. Remove the duplicate field or wire the collector to consume it before relying on these lists as configuration.</comment>

<file context>
@@ -0,0 +1,60 @@
+	"government-eprocure": {
+		name: "Guyana Government eProcure",
+		url: "https://eprocure.gov.gy/",
+		collectorUrls: ["https://eprocure.gov.gy/"],
+		allowedHosts: ["eprocure.gov.gy"],
+		trust: "official-government",
</file context>
Fix with cubic


const service = process.argv[2] as ServiceName | undefined;

if (!service || !(service in services)) {

@cubic-dev-ai cubic-dev-ai Bot Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The in check accepts inherited keys such as constructor and __proto__. Use Object.hasOwn(services, service) so invalid CLI input exits with the documented usage message.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At deploy/mac-mini/service-runner.ts, line 19:

<comment>The `in` check accepts inherited keys such as `constructor` and `__proto__`. Use `Object.hasOwn(services, service)` so invalid CLI input exits with the documented usage message.</comment>

<file context>
@@ -0,0 +1,75 @@
+
+const service = process.argv[2] as ServiceName | undefined;
+
+if (!service || !(service in services)) {
+	console.error("Usage: bun deploy/mac-mini/service-runner.ts <app|api|agent>");
+	process.exit(1);
</file context>
Suggested change
if (!service || !(service in services)) {
if (!service || !Object.hasOwn(services, service)) {
Fix with cubic

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants