MemberJunction / MemberJunction/MJ

Silent realtime-vendor selection, ephemeral magic-link keys, unhelpful class-registration errors, and no owner column on __mj.File

Open
#3,923 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TSQL
Stars
29
Forks
6
Avg merge
2d 1h
Merged PRs (30d)
323

Description

Found on 2026-08-17 standing up a full Open App stack (bizapps-ats + bizapps-caliber + bizapps-forms on one MJAPI/MJExplorer pair) and driving a candidate end to end through a realtime voice session. Five findings against MJ core, all diagnostics or platform gaps rather than engine faults.

Common thread: **the platform knew what was wrong and reported something else**, or reported nothing.

---

## 1. Realtime vendor selection returns null with no diagnostic — a resolvable config error looks like nothing

`selectRealtimeVendor` (packages/AI/Agents, `realtime/realtime-vendor-resolution.ts`) picks the highest-priority Active vendor **whose `DriverClass` resolves an API key**, and returns `null` otherwise. Keys resolve as `AI_VENDOR_API_KEY__`, case-insensitively (`AI/Core/src/generic/apiKeyDictionary.ts`).

**The trap:** `OpenAILLM` and `OpenAIRealtime` are **different driver classes**. A host whose LLM prompts work perfectly — key present, models seeded, realtime vendors `Active` in metadata — still has *zero* usable realtime vendors, and nothing says so until a session tries to launch. On this host that was 10 Active realtime vendors (`OpenAIRealtime` ×3, `GeminiRealtime`, `GrokRealtime`, `HuggingFaceRealtime`, `InworldRealtime`), all skipped silently.

`base-agent.ts` has good text for the *downstream* failure:

> Configure a model of AIModelType 'Realtime' with an active vendor DriverClass and a valid API key (e.g. AI_VENDOR_API_KEY__).

…but selection itself logs nothing, so a caller that consumes the null differently (see the caliber case below) never surfaces even that.

**Fix:** when selection returns null, log which vendors were considered and which env var each would have needed. All of that data is in hand at the point of failure. Consider a one-line boot warning when models of type `Realtime` exist but none resolves a key — that is a misconfiguration, not a valid steady state.

---

## 2. An unset `magicLink.rsaPrivateKey` silently downgrades to an ephemeral keypair

`MagicLinkKeys.ts` generates an ephemeral RS256 keypair when no key is configured. The log line exists, but the behaviour deserves louder treatment than an informational default:

- It is a **data-loss-on-restart** condition — every outstanding magic-link session is invalidated whenever the process restarts.
- It makes everything upstream *appear* correct. Magic-link sessions sign and redeem fine, so intake, claim and enrolment all pass.
- Consumers that read `configInfo.magicLink.rsaPrivateKey` **directly** get `''` and fail in unrelated-looking ways. In our stack that produced a GraphQL `BAD_USER_INPUT` about a null variable, which reads as a caller bug and cost an hour.

**Fix suggestion:** promote to a warning that names the restart consequence, and consider exposing "is the magic-link key ephemeral?" in whatever readiness surface exists, so a downstream package can ask rather than reading the raw config value and drawing its own conclusion.

---

## 3. "Dashboard class 'X' is not registered" never says which package provides it

Full text today:

```
Dashboard class 'FormsHomeDashboard' is not registered. Please check the class registration.
```

The actual cause was that `@mj-biz-apps/forms-ng` **was not installed in the Explorer at all** — the app's migrations had run (so the database seeded nav items naming that `DriverClass`), but the Angular client package was absent. The message sends you to inspect a registration that does not exist rather than to install the thing that provides it.

This is a routine state for a multi-Open-App host, and the same message covers three quite different causes: package not installed, package installed but not referenced (tree-shaken), and a genuine key mismatch.

**Fix:** enumerate the keys that *are* registered for the base class in the error (the ClassFactory already does this elsewhere — we saw a `RequiresSubclass=false` message list every registered key), and if possible name the package that MJ expected. Even "no classes are registered for BaseDashboard from any @mj-biz-apps package" would have pointed straight at it.

Related and worth documenting: `mj codegen manifest` walks a package's **dependencies** and never scans the package's own `src`, so a package's own dashboards can never appear in its own manifest — they must be referenced from the consuming app or tree-shaking drops them. That is subtle, correct, and currently only discoverable by reading a comment in a generated file.

---

## 4. `__mj.File` has no owner or scope column, so file access cannot be row-scoped

At 5.51.0, `MJ: Files` read is flat: `CanRead` grants every file instance-wide, and RLS has no column to key a filter on.

For an ATS this blocks properly-scoped recruiter access to candidate résumés and interview audio — a recruiter at company A can read company B's candidate files, and there is no way to express otherwise. It also blocks the anonymous-upload case: nothing distinguishes a file a respondent genuinely uploaded from a row someone fabricated, so a respondent role cannot safely be granted anything on `MJ: Files` at all.

**Ask:** an owner/scope column (or per-row ACL) on `__mj.File` that RLS can filter on, plus a provenance marker for files created through an anonymous upload path.

Filed here rather than worked around because every consumer needs the same thing and a per-app shim would be worse.

---

## 5. Two smaller ones

**Explorer drops query params for `Custom` nav items.** Deep links into a custom dashboard lose their parameters, so a shared or bookmarked URL lands on the default view. Reproduced against a `Custom` nav item pointing at an Open App dashboard.

**`EntityName` is decorative on typed view resolvers.** The generated `RunViewByName` / dynamic-view resolvers accept an `EntityName` in the input and ignore it — the entity is fixed by the resolver. Passing the wrong one returns the *resolver's* entity with no error, so a copy-paste bug reads as working code. Either honour it or reject a mismatch.

---

Corresponding issues from the same session are being filed on bizapps-caliber, bizapps-ats and bizapps-forms.

Contributor guide

Open the contributing guide

Research direction

Read realtime/realtime-vendor-resolution.ts, AI/Core/src/generic/apiKeyDictionary.ts, MagicLinkKeys.ts, base-agent.ts, and the ClassFactory registration paths first. Then trace Explorer Custom navigation, __mj.File/RLS metadata, and the generated RunViewByName resolvers. Done means each reported failure has a useful diagnostic or defined platform behavior, with regression coverage for the affected paths.

Written by the indexing model from the issue text.

Assessment

Tech stack
angular, graphql, typescript
Domain
api, backend, database, frontend, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.